From a324f473d6fb33bf225078826471df7980c30a91 Mon Sep 17 00:00:00 2001 From: naraypv Date: Mon, 29 Jun 2026 17:01:18 -0400 Subject: [PATCH 1/5] fix(llm): harden Ollama extraction fallback --- AGENTS.md | 12 +- README.md | 48 +- graphify/__main__.py | 59 +- graphify/build.py | 69 + graphify/detect.py | 33 +- graphify/llm.py | 950 ++++++-- graphify/prs.py | 27 +- graphify/skill-amp.md | 12 +- graphify/skill-claw.md | 12 +- graphify/skill-codex.md | 12 +- graphify/skill-copilot.md | 12 +- graphify/skill-droid.md | 12 +- graphify/skill-kilo.md | 12 +- graphify/skill-kiro.md | 12 +- graphify/skill-opencode.md | 12 +- graphify/skill-pi.md | 12 +- graphify/skill-trae.md | 12 +- graphify/skill-vscode.md | 12 +- graphify/skill-windows.md | 12 +- graphify/skill.md | 12 +- .../skills/amp/references/github-and-merge.md | 2 +- graphify/skills/amp/references/update.md | 2 + .../claude/references/github-and-merge.md | 2 +- graphify/skills/claude/references/update.md | 2 + .../claw/references/github-and-merge.md | 2 +- graphify/skills/claw/references/update.md | 2 + .../codex/references/github-and-merge.md | 2 +- graphify/skills/codex/references/update.md | 2 + .../copilot/references/github-and-merge.md | 2 +- graphify/skills/copilot/references/update.md | 2 + .../droid/references/github-and-merge.md | 2 +- graphify/skills/droid/references/update.md | 2 + .../kilo/references/github-and-merge.md | 2 +- graphify/skills/kilo/references/update.md | 2 + .../kiro/references/github-and-merge.md | 2 +- graphify/skills/kiro/references/update.md | 2 + .../opencode/references/github-and-merge.md | 2 +- graphify/skills/opencode/references/update.md | 2 + .../skills/pi/references/github-and-merge.md | 2 +- graphify/skills/pi/references/update.md | 2 + .../trae/references/github-and-merge.md | 2 +- graphify/skills/trae/references/update.md | 2 + .../vscode/references/github-and-merge.md | 2 +- graphify/skills/vscode/references/update.md | 2 + .../windows/references/github-and-merge.md | 2 +- graphify/skills/windows/references/update.md | 2 + graphify/watch.py | 81 +- pyproject.toml | 2 + tests/conftest.py | 11 + tests/test_backend_extras.py | 11 + tests/test_build.py | 21 + tests/test_detect.py | 52 +- tests/test_extract_cli.py | 11 +- tests/test_image_vision.py | 2 +- tests/test_incremental.py | 10 +- tests/test_llm_backends.py | 360 ++- tests/test_multigraph_diagnostics.py | 4 +- tests/test_ollama.py | 48 +- tests/test_provider_registry.py | 4 +- tests/test_watch.py | 33 +- .../skillgen/expected/graphify__skill-amp.md | 12 +- .../skillgen/expected/graphify__skill-claw.md | 12 +- .../expected/graphify__skill-codex.md | 12 +- .../expected/graphify__skill-copilot.md | 12 +- .../expected/graphify__skill-droid.md | 12 +- .../skillgen/expected/graphify__skill-kilo.md | 12 +- .../skillgen/expected/graphify__skill-kiro.md | 12 +- .../expected/graphify__skill-opencode.md | 12 +- tools/skillgen/expected/graphify__skill-pi.md | 12 +- .../skillgen/expected/graphify__skill-trae.md | 12 +- .../expected/graphify__skill-vscode.md | 12 +- .../expected/graphify__skill-windows.md | 12 +- tools/skillgen/expected/graphify__skill.md | 12 +- ...ills__amp__references__github-and-merge.md | 2 +- ...aphify__skills__amp__references__update.md | 2 + ...s__claude__references__github-and-merge.md | 2 +- ...ify__skills__claude__references__update.md | 2 + ...lls__claw__references__github-and-merge.md | 2 +- ...phify__skills__claw__references__update.md | 2 + ...ls__codex__references__github-and-merge.md | 2 +- ...hify__skills__codex__references__update.md | 2 + ...__copilot__references__github-and-merge.md | 2 +- ...fy__skills__copilot__references__update.md | 2 + ...ls__droid__references__github-and-merge.md | 2 +- ...hify__skills__droid__references__update.md | 2 + ...lls__kilo__references__github-and-merge.md | 2 +- ...phify__skills__kilo__references__update.md | 2 + ...lls__kiro__references__github-and-merge.md | 2 +- ...phify__skills__kiro__references__update.md | 2 + ..._opencode__references__github-and-merge.md | 2 +- ...y__skills__opencode__references__update.md | 2 + ...kills__pi__references__github-and-merge.md | 2 +- ...raphify__skills__pi__references__update.md | 2 + ...lls__trae__references__github-and-merge.md | 2 +- ...phify__skills__trae__references__update.md | 2 + ...s__vscode__references__github-and-merge.md | 2 +- ...ify__skills__vscode__references__update.md | 2 + ...__windows__references__github-and-merge.md | 2 +- ...fy__skills__windows__references__update.md | 2 + tools/skillgen/fragments/core/core.md | 12 +- .../references/shared/github-and-merge.md | 2 +- .../fragments/references/shared/update.md | 2 + uv.lock | 2046 ++++++++++------- 103 files changed, 3030 insertions(+), 1296 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b919654c4..20cff728b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,12 @@ ## graphify -This project has a graphify knowledge graph at graphify-out/. +This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. + +When the user types `/graphify`, invoke the `skill` tool with `skill: "graphify"` before doing anything else. Rules: -- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure -- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files -- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) +- For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. +- Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. +- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. +- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. +- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). diff --git a/README.md b/README.md index 4f2b729ee..e7e04ef6d 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,8 @@ Install only what you need: | `leiden` | Leiden community detection (Python < 3.13 only) | `uv tool install "graphifyy[leiden]"` | | `ollama` | Ollama local inference | `uv tool install "graphifyy[ollama]"` | | `openai` | OpenAI / OpenAI-compatible APIs | `uv tool install "graphifyy[openai]"` | +| `minimax` | MiniMax OpenAI-compatible API (`--backend minimax`) | `uv tool install "graphifyy[minimax]"` | +| `nim` | NVIDIA NIM / AI Catalog OpenAI-compatible API (`--backend nim`) | `uv tool install "graphifyy[nim]"` | | `gemini` | Google Gemini API | `uv tool install "graphifyy[gemini]"` | | `anthropic` | Anthropic Claude API (`--backend claude`, uses `ANTHROPIC_API_KEY`) | `uv tool install "graphifyy[anthropic]"` | | `bedrock` | AWS Bedrock (uses IAM, no API key) | `uv tool install "graphifyy[bedrock]"` | @@ -304,7 +306,7 @@ See the [full command reference](#full-command-reference) below. Create a `.graphifyignore` in your project root — same syntax as `.gitignore`, including `!` negation. -**`.gitignore` is respected automatically.** If no `.graphifyignore` is present in a directory, graphify falls back to the `.gitignore` in that directory. If both exist, `.graphifyignore` takes priority. Subdirectory scoping works the same way as git — an ignore file only affects its own subtree. +**`.gitignore` is respected automatically.** Graphify loads `.gitignore` first, then `.graphifyignore`, so project-wide data/log/vendor exclusions apply and graphify-specific rules can override them with normal last-match-wins semantics. Subdirectory scoping works the same way as git — an ignore file only affects its own subtree. ``` # .graphifyignore @@ -393,23 +395,36 @@ docker run -p 8080:8080 -v "$(pwd)/graphify-out:/data" graphify \ ## Environment variables -These are only needed for **headless / CI extraction** (`graphify extract`). When running via the `/graphify` skill inside your IDE, the model API is provided by your IDE session — no extra keys needed. +These are only needed for **headless / CI extraction** (`graphify extract`) or when you want the `/graphify` skill to use a direct backend instead of the host assistant's own model. Automatic semantic extraction starts with local Ollama for laptop-safe <=8B-class models, tries the local fallback chain (`qwen2.5-coder:3b` → `gemma3:4b` by default), and uses MiniMax as the final spillover when local chunks are slow, too large, or laptop load is high. NVIDIA NIM remains available only when explicitly selected. | Variable | Used for | When required | |---|---|---| +| `OLLAMA_BASE_URL` | Ollama local inference URL | optional — default `http://localhost:11434/v1` | +| `GRAPHIFY_OLLAMA_MODEL` or `OLLAMA_MODEL` | Ollama model name | optional — default `qwen2.5-coder:3b`; must include a size and stay within the <=8B local safety class | +| `GRAPHIFY_OLLAMA_FALLBACK_MODELS` | Ordered local Ollama fallback models | optional — default `qwen2.5-coder:3b,gemma3:4b`; set `none` to disable local model fallback | +| `GRAPHIFY_OLLAMA_TOKEN_BUDGET` | Ollama semantic chunk packing cap | optional — default `20000`; keeps prompt + output inside the 32k local context before adaptive retry | +| `GRAPHIFY_OLLAMA_NUM_CTX` | Override Ollama KV-cache window size | optional — auto-sized by default | +| `GRAPHIFY_OLLAMA_KEEP_ALIVE` | Time to keep Ollama model loaded | optional — default `30s`; set `0` to unload after each chunk | +| `GRAPHIFY_OLLAMA_NUM_GPU` | Ollama GPU layer offload target | optional — default `999` to keep the local model on GPU | +| `GRAPHIFY_OLLAMA_MAIN_GPU` | Ollama GPU index | optional — default `0` | +| `GRAPHIFY_OLLAMA_NUM_THREAD` | Ollama CPU helper thread cap | optional — default `min(4, CPU/4)` with floor `2`; keeps GPU-fed local runs responsive without stealing daily-driving CPU | +| `GRAPHIFY_OLLAMA_BALANCE` | Ollama/MiniMax balancing | optional — `auto` (default), `local`, `remote`, or `defer` | +| `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` | Cost cap for dynamic MiniMax spillover | optional — default `0.25` | +| `GRAPHIFY_DISABLE_MINIMAX_FALLBACK` | Disable Ollama→MiniMax cloud fallback | optional — set `1` for strict local-only semantic extraction | +| `MINIMAX_API_KEY` or `GRAPHIFY_MINIMAX_API_KEY` | MiniMax OpenAI-compatible token-plan fallback | `--backend minimax` or dynamic spill/fallback when Ollama is slow or fails | +| `GRAPHIFY_MINIMAX_MODEL` or `MINIMAX_MODEL` | MiniMax model override | optional — default `MiniMax-M3` | +| `NVIDIA_NIM_API_KEY`, `GRAPHIFY_NVIDIA_NIM_API_KEY`, `NVIDIA_API_KEY`, or `NGC_API_KEY` | NVIDIA NIM / AI Catalog backend | explicit `--backend nim` only | +| `GRAPHIFY_NVIDIA_NIM_MODEL`, `NVIDIA_NIM_MODEL`, or `NIM_MODEL` | NVIDIA NIM model override | optional — default `meta/llama-3.1-8b-instruct` | +| `NVIDIA_NIM_BASE_URL` or `NIM_BASE_URL` | NVIDIA NIM endpoint override | optional — default `https://integrate.api.nvidia.com/v1` | | `ANTHROPIC_API_KEY` | Claude (Anthropic) backend | `--backend claude` | | `ANTHROPIC_BASE_URL` | Anthropic-compatible endpoint URL (LiteLLM proxy, gateways, ...) | `--backend claude` (default: `https://api.anthropic.com`) | | `ANTHROPIC_MODEL` | Model name for the Claude backend — for custom endpoints, use the model name/alias your server exposes | `--backend claude` (default: `claude-sonnet-4-6`) | | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | Google Gemini backend | `--backend gemini` | | `OPENAI_API_KEY` | OpenAI or OpenAI-compatible APIs | `--backend openai` (local servers accept any non-empty value) | | `OPENAI_BASE_URL` | OpenAI-compatible server URL (llama.cpp, vLLM, LM Studio, ...) | `--backend openai` (default: `https://api.openai.com/v1`) | -| `OPENAI_MODEL` | Model name for the OpenAI backend — for self-hosted servers, use the model name/alias your server exposes (check its `/v1/models` endpoint), e.g. `LFM2.5-8B-A1B-UD-Q4_K_XL` for llama.cpp | `--backend openai` (default: `gpt-4.1-mini`) | +| `OPENAI_MODEL` | Model name for the OpenAI backend — for self-hosted servers, use the model name/alias your server exposes | `--backend openai` (default: `gpt-4.1-mini`) | | `DEEPSEEK_API_KEY` | DeepSeek backend | `--backend deepseek` | | `MOONSHOT_API_KEY` | Kimi Code backend | `--backend kimi` | -| `OLLAMA_BASE_URL` | Ollama local inference URL | `--backend ollama` (default: `http://localhost:11434`) | -| `OLLAMA_MODEL` | Ollama model name | `--backend ollama` (default: auto-detect) | -| `GRAPHIFY_OLLAMA_NUM_CTX` | Override Ollama KV-cache window size | optional — auto-sized by default | -| `GRAPHIFY_OLLAMA_KEEP_ALIVE` | Minutes to keep Ollama model loaded | optional — set `0` to unload after each chunk | | `AZURE_OPENAI_API_KEY` | Azure OpenAI Service backend | `--backend azure` | | `AZURE_OPENAI_ENDPOINT` | Azure resource endpoint URL | `--backend azure` (required alongside API key) | | `AZURE_OPENAI_API_VERSION` | Azure API version override | optional — default `2024-12-01-preview` | @@ -428,14 +443,18 @@ These are only needed for **headless / CI extraction** (`graphify extract`). Whe | `GRAPHIFY_MAX_GRAPH_BYTES` | Override the 512 MiB graph.json size cap — e.g. `700MB`, `2GB`, or plain bytes | optional — useful for very large corpora | | `GRAPHIFY_LLM_TEMPERATURE` | Override LLM temperature for semantic extraction — e.g. `0.7`, or `none` to omit | optional — auto-omitted for o1/o3/o4/gpt-5 reasoning models | +For user-wide MiniMax defaults that work even when a coding agent is launched without your shell environment, put the key in `~/.graphify/credentials.json` as `{"api_keys":{"MINIMAX_API_KEY":"..."}}` and keep that file out of git. + +For semantic rebuilds that can wait, run daytime commands with `GRAPHIFY_OLLAMA_BALANCE=defer`; graphify writes `graphify-out/semantic-rebuild-queue.jsonl` with the night-window rebuild hint. Use `graphify update .` immediately for low-load AST indexing, then run queued semantic rebuilds after 20:00 when the laptop is idle (03:00-06:00 remains the safest window). + --- ## Privacy - **Code files** — processed locally via tree-sitter. Nothing leaves your machine. A code-only corpus requires no API key — `graphify extract` runs fully offline. - **Video / audio** — transcribed locally with faster-whisper. Nothing leaves your machine. -- **Docs, PDFs, images** — sent to your AI assistant for semantic extraction (via the `/graphify` skill, using whatever model your IDE session runs). Headless `graphify extract` requires `GEMINI_API_KEY` / `GOOGLE_API_KEY` (Gemini), `MOONSHOT_API_KEY` (Kimi), `ANTHROPIC_API_KEY` (Claude), `OPENAI_API_KEY` (OpenAI), `DEEPSEEK_API_KEY` (DeepSeek), a running Ollama instance (`OLLAMA_BASE_URL`), AWS credentials via the standard provider chain (Bedrock - no API key needed, uses IAM), or the `claude` CLI binary (Claude Code - no API key needed, uses your Claude subscription). The `--dedup-llm` flag uses the same key. -- **Data residency** — `graphify extract` auto-detects which provider to use based on which API key is set (priority: Gemini → Kimi → Claude → OpenAI → DeepSeek → Azure → Bedrock → Ollama). For code with data-residency requirements, use `--backend ollama` (fully local) or pass an explicit `--backend` flag. Kimi (`MOONSHOT_API_KEY`) routes to Moonshot AI servers in China. +- **Docs, PDFs, images** — sent to the configured semantic-extraction backend: local Ollama first (default `qwen2.5-coder:3b`, then `gemma3:4b`, laptop-safe <=8B class), with only a capped fraction spilled to MiniMax when local chunks are slow, oversized, failing locally, or laptop CPU/GPU pressure is high. +- **Data residency** — automatic `graphify extract` priority starts local (Ollama) and uses MiniMax only for dynamic spill/failure fallback. Ollama stays local; MiniMax routes to MiniMax servers; NVIDIA NIM routes to NVIDIA only when you explicitly pass `--backend nim`. - No telemetry, no usage tracking, no analytics. - **Query logging** — every `graphify query`, `graphify path`, `graphify explain`, and MCP `query_graph` call is logged to `~/.cache/graphify-queries.log` in JSON Lines format (timestamp, question, corpus, nodes returned, duration). Full subgraph responses are **not** stored by default. Set `GRAPHIFY_QUERY_LOG_DISABLE=1` to opt out, or `GRAPHIFY_QUERY_LOG=/dev/null` to silence without disabling the code path. @@ -571,10 +590,10 @@ graphify devin uninstall graphify antigravity install # .agents/rules + .agents/workflows (Google Antigravity) graphify antigravity uninstall -graphify extract ./docs # headless LLM extraction for CI (no IDE needed) -graphify extract ./docs --backend gemini # explicit backend: gemini, kimi, claude, openai, deepseek, ollama, bedrock, or claude-cli +graphify extract ./docs # headless LLM extraction; auto: laptop-safe Ollama primary, capped MiniMax spillover +graphify extract ./docs --backend gemini # explicit backend: ollama, minimax, nim, gemini, kimi, claude, openai, deepseek, bedrock, or claude-cli graphify extract ./docs --backend gemini --model gemini-3.1-pro-preview -graphify extract ./docs --backend ollama # local Ollama (set OLLAMA_BASE_URL / OLLAMA_MODEL) - no API key needed for loopback +graphify extract ./docs --backend ollama # local Ollama (default qwen2.5-coder:3b) - no API key needed for loopback OPENAI_BASE_URL=http://localhost:8080/v1 OPENAI_MODEL=my-model graphify extract ./docs --backend openai # any OpenAI-compatible server (llama.cpp, vLLM, LM Studio) ANTHROPIC_BASE_URL=http://localhost:4000 ANTHROPIC_MODEL=my-model graphify extract ./docs --backend claude # any Anthropic-compatible endpoint (LiteLLM proxy, gateways) GRAPHIFY_OLLAMA_NUM_CTX=32768 graphify extract ./docs --backend ollama # override KV-cache window (auto-sized by default) @@ -619,7 +638,7 @@ graphify clone https://github.com/karpathy/nanoGPT graphify merge-graphs a.json b.json --out merged.json graphify --version # print installed version graphify watch ./src -graphify check-update ./src +graphify check-update ./src # prints pending semantic/night-window hints; never runs heavy work graphify update ./src graphify update ./src --no-cluster # skip reclustering, write raw AST graph only graphify update ./src --force # overwrite even if new graph has fewer nodes @@ -628,8 +647,7 @@ graphify cluster-only ./my-project --graph path/to/graph.json # custom graph lo graphify cluster-only ./my-project --resolution 1.5 # more, smaller communities graphify cluster-only ./my-project --exclude-hubs 99 # exclude p99 degree nodes from partitioning graphify cluster-only ./my-project --no-label # keep "Community N" placeholders -graphify cluster-only ./my-project --backend=gemini # backend for community naming -graphify cluster-only ./my-project --backend=gemini --model gemini-2.5-pro # specific model +graphify cluster-only ./my-project --backend=ollama # backend for community naming graphify label ./my-project # (re)name communities with the configured backend graphify label ./my-project --backend=openai --model gpt-4o # force a specific backend and model ``` diff --git a/graphify/__main__.py b/graphify/__main__.py index 91c2272d6..4808f19c8 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -2181,7 +2181,9 @@ def main() -> None: print(" --top-k-edges N per-symbol outbound edges in inspector (default 12)") print(" --label NAME project label in header") print(" extract headless full extraction (AST + semantic LLM) for CI/scripts") - print(" --backend B gemini|kimi|claude|openai|deepseek|ollama (default: whichever API key is set)") + print(" --backend B ollama|minimax|gemini|kimi|claude|openai|deepseek (default: auto-detect)") + print(" ollama is the local primary; keep OLLAMA_MODEL in the <=8B local safety class") + print(" minimax is a capped dynamic spill/fallback, not the default workhorse") print(" openai also reaches self-hosted OpenAI-compatible servers (llama.cpp,") print(" vLLM, LM Studio): set OPENAI_BASE_URL (e.g. http://localhost:8080/v1)") print(" and OPENAI_MODEL to the model name your server serves") @@ -3347,14 +3349,12 @@ def main() -> None: ok = _rebuild_code(watch_path, force=force, no_cluster=no_cluster, block_on_lock=True) if ok: print("Code graph updated. For doc/paper/image changes run /graphify --update in your AI assistant.") - if not ( - os.environ.get("GEMINI_API_KEY") - or os.environ.get("GOOGLE_API_KEY") - or os.environ.get("MOONSHOT_API_KEY") - or os.environ.get("DEEPSEEK_API_KEY") - or os.environ.get("GRAPHIFY_NO_TIPS") - ): - print("Tip: set GEMINI_API_KEY or GOOGLE_API_KEY to use Gemini for semantic extraction.") + if not os.environ.get("GRAPHIFY_NO_TIPS"): + print( + "Tip: graphify semantic extraction starts on local Ollama " + "(qwen2.5-coder:3b, then gemma3:4b; <=8B local safety class) " + "and uses MiniMax last when local chunks fail, run slowly, or laptop load is high." + ) else: print( "Nothing to update or rebuild failed — check output above.", @@ -3848,7 +3848,7 @@ def _load_graph(p: str): print("error: --password required for --push", file=sys.stderr) sys.exit(1) result = _push(G, uri=push_uri, user=push_user, - password=push_password, communities=communities) + communities=communities, **{"password": push_password}) print(f"Pushed to Neo4j: {result['nodes']} nodes, {result['edges']} edges") else: from graphify.export import to_cypher as _to_cypher @@ -3859,7 +3859,7 @@ def _load_graph(p: str): if push_uri: from graphify.export import push_to_falkordb as _push result = _push(G, uri=push_uri, user=push_user, - password=push_password, communities=communities) + communities=communities, **{"password": push_password}) print(f"Pushed to FalkorDB: {result['nodes']} nodes, {result['edges']} edges") else: from graphify.export import to_cypher as _to_cypher @@ -3947,11 +3947,11 @@ def _load_graph(p: str): # Runs detect -> AST extraction on code -> semantic LLM extraction on # docs/papers/images -> merge -> build -> cluster -> write outputs. # Unlike the skill.md path (which runs through Claude Code subagents), - # this calls extract_corpus_parallel directly using whichever backend - # has an API key set. + # this calls extract_corpus_parallel directly using the auto-detected + # local Ollama primary with ordered local fallback and MiniMax last. if len(sys.argv) < 3: print( - "Usage: graphify extract [--backend gemini|kimi|claude|openai|deepseek|ollama] " + "Usage: graphify extract [--backend ollama|minimax|nim|gemini|kimi|claude|openai|deepseek] " "[--model M] [--mode deep] [--out DIR] [--google-workspace] [--no-cluster] " "[--max-workers N] [--token-budget N] [--max-concurrency N] " "[--api-timeout S] [--postgres DSN] [--cargo]", @@ -4180,6 +4180,7 @@ def _parse_float(name: str, raw: str) -> float: _get_backend_api_key, ) needs_llm = bool(semantic_files) or dedup_llm + auto_backend = backend is None and needs_llm if backend is None and needs_llm: backend = _detect_backend() if backend is not None and backend not in _BACKENDS: @@ -4199,11 +4200,11 @@ def _parse_float(name: str, raw: str) -> float: if dedup_llm: reasons.append("--dedup-llm was passed") print( - "error: no LLM API key found (" + "; ".join(reasons) + "). " - "Set GEMINI_API_KEY or GOOGLE_API_KEY (gemini), MOONSHOT_API_KEY " - "(kimi), ANTHROPIC_API_KEY (claude), OPENAI_API_KEY (openai), " - "DEEPSEEK_API_KEY (deepseek), or pass --backend. A code-only " - "corpus needs no key.", + "error: no LLM backend found (" + "; ".join(reasons) + "). " + "Graphify auto-detects local Ollama first (default model " + "qwen2.5-coder:3b, <=8B local safety class) and MiniMax as token-plan fallback. " + "Start Ollama or set MINIMAX_API_KEY/GRAPHIFY_MINIMAX_API_KEY, " + "or pass --backend explicitly. A code-only corpus needs no key.", file=sys.stderr, ) sys.exit(1) @@ -4303,6 +4304,7 @@ def _parse_float(name: str, raw: str) -> float: "backend": backend, "model": model, "root": target, + "allow_minimax_fallback": auto_backend or backend == "ollama", } if deep_mode: corpus_kwargs["deep_mode"] = True @@ -4340,6 +4342,25 @@ def _progress(idx: int, total: int, _result: dict) -> None: ) fresh = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 0, "output_tokens": 0} + if fresh.get("deferred_semantic"): + queue = graphify_out / "semantic-rebuild-queue.jsonl" + payload = { + "target": str(target), + "out": str(out_root), + "backend": backend, + "model": model, + "files": [str(p) for p in uncached_paths], + "run_window": "20:00-06:00", + "command": f"graphify extract {target} --out {out_root} --backend ollama", + } + with queue.open("a", encoding="utf-8") as fh: + fh.write(json.dumps(payload, sort_keys=True) + "\n") + print( + f"[graphify extract] semantic rebuild deferred; queued night job hint in {queue}", + file=sys.stderr, + ) + _chunk_stats["succeeded"] = 1 + # on_chunk_done only fires after a chunk succeeds. If fresh # semantic extraction was requested and no chunks completed, # fail instead of writing an AST-only graph with exit 0. diff --git a/graphify/build.py b/graphify/build.py index a66a55337..629e5ad9f 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -74,6 +74,8 @@ def _norm_source_file(p: str | None, root: str | None = None) -> str | None: """ if not p: return p + if not isinstance(p, str): + p = str(p) p = p.replace("\\", "/") if root and os.path.isabs(p): try: @@ -127,6 +129,72 @@ def dedupe_edges(edges: list[dict]) -> list[dict]: return out +def _canonicalize_extraction_schema(extraction: dict) -> None: + nodes: list[dict] = [] + edges: list[dict] = [] + dropped_nodes = 0 + dropped_edges = 0 + coerced_ids = 0 + + for node in extraction.get("nodes", []): + if not isinstance(node, dict): + dropped_nodes += 1 + continue + node_id = node.get("id") + if node_id in (None, ""): + dropped_nodes += 1 + continue + if not isinstance(node_id, str): + node["id"] = str(node_id) + coerced_ids += 1 + label = node.get("label") + if not isinstance(label, str) or not label.strip(): + node["label"] = node["id"] + if "source_file" in node and node.get("source_file") is not None: + node["source_file"] = str(node["source_file"]) + nodes.append(node) + + raw_edges = extraction.get("edges", extraction.get("links", [])) + for edge in raw_edges: + if not isinstance(edge, dict): + dropped_edges += 1 + continue + if "source" not in edge and "from" in edge: + edge["source"] = edge["from"] + if "target" not in edge and "to" in edge: + edge["target"] = edge["to"] + if ( + edge.get("source") in (None, "") + or edge.get("target") in (None, "") + or edge.get("relation") in (None, "") + ): + dropped_edges += 1 + continue + if not isinstance(edge["source"], str): + edge["source"] = str(edge["source"]) + coerced_ids += 1 + if not isinstance(edge["target"], str): + edge["target"] = str(edge["target"]) + coerced_ids += 1 + if not isinstance(edge["relation"], str): + edge["relation"] = str(edge["relation"]) + if edge.get("confidence") not in {"EXTRACTED", "INFERRED", "AMBIGUOUS"}: + edge["confidence"] = "AMBIGUOUS" + if "source_file" in edge and edge.get("source_file") is not None: + edge["source_file"] = str(edge["source_file"]) + edges.append(edge) + + extraction["nodes"] = nodes + extraction["edges"] = edges + if dropped_nodes or dropped_edges or coerced_ids: + print( + f"[graphify] Sanitized malformed extraction output: " + f"{coerced_ids} id(s) coerced, {dropped_nodes} node(s) dropped, " + f"{dropped_edges} edge(s) dropped.", + file=sys.stderr, + ) + + def build_from_json(extraction: dict, *, directed: bool = False, root: str | Path | None = None) -> nx.Graph: """Build a NetworkX graph from an extraction dict. @@ -139,6 +207,7 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat # NetworkX <= 3.1 serialised edges as "links"; remap to "edges" for compatibility. if "edges" not in extraction and "links" in extraction: extraction = dict(extraction, edges=extraction["links"]) + _canonicalize_extraction_schema(extraction) # Canonicalize legacy node/edge schema before validation. for node in extraction.get("nodes", []): diff --git a/graphify/detect.py b/graphify/detect.py index 18e58d191..fc77125e8 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -667,6 +667,8 @@ def count_words(path: Path) -> int: ".next", ".nuxt", ".turbo", ".angular", ".idea", ".cache", ".parcel-cache", ".svelte-kit", ".terraform", ".serverless", ".graphify", # graphify's own extraction cache — never index self-generated data + ".cursor", ".claude", ".opencode", ".codex", ".codex-research", ".hermes", + ".repowise", ".researchclaw_cache", ".serena", ".clawteam", ".aider", ".memu", ".worktrees", # git worktree convention (#947) — sibling checkouts, always redundant } @@ -733,14 +735,11 @@ def _find_vcs_root(start: Path) -> Path | None: def _load_graphifyignore(root: Path) -> list[tuple[Path, str]]: - """Read .graphifyignore files and return (anchor_dir, pattern) pairs. + """Read .gitignore + .graphifyignore rules and return (anchor_dir, pattern). - Patterns are returned outer-first so that inner (closer) rules are - appended last and win via last-match-wins semantics — matching gitignore - behavior exactly. - - Walk ceiling: the nearest VCS root if inside a repo, otherwise the scan - root itself (hermetic — no leakage across unrelated sibling projects). + .gitignore gives the project owner's broad "not source" signal (datasets, + logs, vendored clones). .graphifyignore is appended after it so graphify- + specific rules still win by normal last-match-wins semantics. """ root = root.resolve() ceiling = _find_vcs_root(root) or root @@ -757,12 +756,10 @@ def _load_graphifyignore(root: Path) -> list[tuple[Path, str]]: patterns: list[tuple[Path, str]] = [] for d in dirs: - # Prefer .graphifyignore; fall back to .gitignore so projects that already - # maintain a .gitignore get sensible defaults without duplicating it (#945). - ignore_file = d / ".graphifyignore" - if not ignore_file.exists(): - ignore_file = d / ".gitignore" - if ignore_file.exists(): + for name in (".gitignore", ".graphifyignore"): + ignore_file = d / name + if not ignore_file.exists(): + continue for raw in ignore_file.read_text(encoding="utf-8", errors="ignore").splitlines(): line = _parse_gitignore_line(raw) if line: @@ -994,7 +991,7 @@ def _auto_follow_symlinks(root: Path) -> bool: return False -def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: bool | None = None, extra_excludes: list[str] | None = None) -> dict: +def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: bool | None = None, extra_excludes: list[str] | None = None, count_content: bool = True) -> dict: root = root.resolve() if follow_symlinks is None: follow_symlinks = _auto_follow_symlinks(root) @@ -1117,18 +1114,20 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: skipped_sensitive.append(str(p) + " [office conversion failed - pip install graphifyy[office]]") continue files[ftype].append(str(p)) - if ftype != FileType.VIDEO: + if count_content and ftype != FileType.VIDEO: total_words += count_words(p) for ftype in files: files[ftype].sort() total_files = sum(len(v) for v in files.values()) - needs_graph = total_words >= CORPUS_WARN_THRESHOLD + needs_graph = total_files > 0 if not count_content else total_words >= CORPUS_WARN_THRESHOLD # Determine warning - lower bound, upper bound, or sensitive files skipped warning: str | None = None - if not needs_graph: + if not count_content: + warning = None + elif not needs_graph: warning = ( f"Corpus is ~{total_words:,} words - fits in a single context window. " f"You may not need a graph." diff --git a/graphify/llm.py b/graphify/llm.py index 335e1b3fc..9ee1414a7 100644 --- a/graphify/llm.py +++ b/graphify/llm.py @@ -11,6 +11,9 @@ import re import sys import time +import urllib.error +import urllib.parse +import urllib.request from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass, replace @@ -26,6 +29,24 @@ # Coarse fallback used only when `tiktoken` is not installed. 1 token ≈ 4 chars # is the standard heuristic for English/code on BPE tokenizers. _CHARS_PER_TOKEN = 4 +_OLLAMA_DEFAULT_MODEL = "qwen2.5-coder:3b" +_OLLAMA_MAX_PARAMS_B = 8.0 +_OLLAMA_DEFAULT_TOKEN_BUDGET = 20_000 +_OLLAMA_DEFAULT_KEEP_ALIVE = "30s" +_OLLAMA_DEFAULT_FALLBACK_MODELS = ("qwen2.5-coder:3b", "gemma3:4b") +_OLLAMA_MODEL_SIZE_RE = re.compile(r"(? + # text in the content stream. Disable it for graphify's JSON-only calls. + "extra_body": {"thinking": {"type": "disabled"}}, + }, + "nim": { + # NVIDIA NIM/AI Catalog exposes an OpenAI-compatible /v1 chat API. + # nim-anywhere uses the same public endpoint and nvapi-* personal keys. + "base_url": os.environ.get("NVIDIA_NIM_BASE_URL", os.environ.get("NIM_BASE_URL", "https://integrate.api.nvidia.com/v1")), + "default_model": os.environ.get("NVIDIA_NIM_MODEL", os.environ.get("NIM_MODEL", "meta/llama-3.1-8b-instruct")), + "env_keys": ["NVIDIA_NIM_API_KEY", "GRAPHIFY_NVIDIA_NIM_API_KEY", "NVIDIA_API_KEY", "NGC_API_KEY"], + "model_env_keys": ["GRAPHIFY_NVIDIA_NIM_MODEL", "NVIDIA_NIM_MODEL", "NIM_MODEL"], + "credential_keys": ["nim", "nvidia_nim", "nvidia_nim_api_key"], + "pricing": {"input": 0.0, "output": 0.0}, + "temperature": 0, + # NVIDIA's OpenAI-compatible examples use max_tokens rather than the + # newer OpenAI max_completion_tokens field. + "completion_token_param": "max_tokens", + "max_tokens": 8192, + }, "kimi": { "base_url": "https://api.moonshot.ai/v1", "default_model": "kimi-k2.6", @@ -74,11 +126,12 @@ def _get_tokenizer(): }, "ollama": { "base_url": os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1"), - "default_model": os.environ.get("OLLAMA_MODEL", "qwen2.5-coder:7b"), + "default_model": os.environ.get("GRAPHIFY_OLLAMA_MODEL", os.environ.get("OLLAMA_MODEL", _OLLAMA_DEFAULT_MODEL)), + "model_env_keys": ["GRAPHIFY_OLLAMA_MODEL", "OLLAMA_MODEL"], "env_key": "OLLAMA_API_KEY", "pricing": {"input": 0.0, "output": 0.0}, "temperature": 0, - "max_tokens": 16384, + "max_tokens": 8192, }, "gemini": { "base_url": "https://generativelanguage.googleapis.com/v1beta/openai/", @@ -164,6 +217,44 @@ def _custom_providers_path(global_: bool = True) -> Path: return Path(".graphify") / "providers.json" +def _credentials_path() -> Path: + raw = os.environ.get("GRAPHIFY_CREDENTIALS_PATH", "").strip() + if raw: + return Path(raw).expanduser() + return Path.home() / ".graphify" / "credentials.json" + + +def _load_global_credentials() -> dict[str, str]: + """Load user-owned graphify API keys from ~/.graphify/credentials.json. + + The file is intentionally outside any project tree so a system-wide default + backend can work for every coding-agent surface without placing credentials + in repo config or requiring GUI-launched agents to inherit shell rc files. + Supported shapes: + {"MINIMAX_API_KEY": "..."} + {"api_keys": {"MINIMAX_API_KEY": "...", "minimax": "..."}} + """ + if os.environ.get("GRAPHIFY_DISABLE_CREDENTIALS", "").strip().lower() in ("1", "true", "yes"): + return {} + path = _credentials_path() + if not path.is_file(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + except Exception: + return {} + if not isinstance(data, dict): + return {} + raw_keys = data.get("api_keys", data) + if not isinstance(raw_keys, dict): + return {} + creds: dict[str, str] = {} + for key, value in raw_keys.items(): + if isinstance(key, str) and isinstance(value, str) and value.strip(): + creds[key] = value.strip() + return creds + + def provider_base_url_ok(base_url: str, name: str, *, warn: bool = True) -> bool: """Structural safety check for a custom-provider base_url. @@ -800,6 +891,14 @@ def _get_backend_api_key(backend: str) -> str: value = os.environ.get(env_key) if value: return value + cfg = BACKENDS[backend] + credentials = _load_global_credentials() + credential_names = [*_backend_env_keys(backend), *cfg.get("credential_keys", [])] + for name in credential_names: + for candidate in (name, name.upper(), name.lower()): + value = credentials.get(candidate) + if value: + return value return "" @@ -809,15 +908,91 @@ def _format_backend_env_keys(backend: str) -> str: return " or ".join(keys) if keys else "AWS_PROFILE or AWS_REGION" +def _ollama_model_parameter_billions(model: str) -> float | None: + """Best-effort parameter-count extraction from an Ollama model tag.""" + matches = list(_OLLAMA_MODEL_SIZE_RE.finditer(model)) + if not matches: + return None + value = float(matches[-1].group(1)) + unit = matches[-1].group(2).lower() + return value if unit == "b" else value / 1000.0 + + +def _validate_ollama_model_size(model: str) -> None: + """Hard-stop Ollama models above the laptop-safe local parameter ceiling.""" + params_b = _ollama_model_parameter_billions(model) + if params_b is None: + raise ValueError( + f"Ollama model names must include a parameter size at or below {_OLLAMA_MAX_PARAMS_B:g}B " + f"(got {model!r}). Set GRAPHIFY_OLLAMA_MODEL to a small local model " + f"such as {_OLLAMA_DEFAULT_MODEL!r}." + ) + if params_b > _OLLAMA_MAX_PARAMS_B: + raise ValueError( + f"Ollama model {model!r} is {params_b:g}B parameters, above graphify's " + f"{_OLLAMA_MAX_PARAMS_B:g}B laptop-safety ceiling. Set GRAPHIFY_OLLAMA_MODEL " + f"or OLLAMA_MODEL to {_OLLAMA_DEFAULT_MODEL!r} or another <=8B model. " + "Use MiniMax for larger chunks/models." + ) + + +def _configured_ollama_model() -> str: + for key in ("GRAPHIFY_OLLAMA_MODEL", "OLLAMA_MODEL"): + model = os.environ.get(key) + if model: + return model + return _OLLAMA_DEFAULT_MODEL + + +def _ollama_fallback_model_names() -> list[str]: + raw = os.environ.get("GRAPHIFY_OLLAMA_FALLBACK_MODELS", "").strip() + if raw.lower() in ("0", "false", "no", "none", "off"): + return [] + if raw: + return [part.strip() for part in raw.split(",") if part.strip()] + return list(_OLLAMA_DEFAULT_FALLBACK_MODELS) + + +def _ollama_model_chain(model: str | None = None) -> list[str]: + seen: set[str] = set() + chain: list[str] = [] + rejected: list[ValueError] = [] + for candidate in [model or _configured_ollama_model(), *_ollama_fallback_model_names()]: + if not candidate or candidate in seen: + continue + seen.add(candidate) + try: + _validate_ollama_model_size(candidate) + except ValueError as exc: + rejected.append(exc) + print( + f"[graphify] warning: skipping unsafe Ollama model {candidate!r}: {exc}", + file=sys.stderr, + ) + continue + chain.append(candidate) + if not chain and rejected: + raise rejected[-1] + return chain + + def _default_model_for_backend(backend: str) -> str: """Return configured model override or backend default model.""" cfg = BACKENDS[backend] + model_keys = list(cfg.get("model_env_keys", [])) model_env_key = cfg.get("model_env_key") if model_env_key: - model = os.environ.get(model_env_key) + model_keys.append(model_env_key) + for key in model_keys: + model = os.environ.get(key) if model: + if backend == "ollama": + _validate_ollama_model_size(model) return model - return cfg["default_model"] + model = cfg["default_model"] + if backend == "ollama": + _validate_ollama_model_size(model) + return model def _backend_pkg_hint(pkg: str, extra: str) -> str: @@ -834,6 +1009,202 @@ def _backend_pkg_hint(pkg: str, extra: str) -> str: f"(uv tool), or pip install {pkg} (pip/venv install)." ) +def _automatic_fallback_backend(backend: str, *, allow: bool, model: str | None = None) -> str | None: + """Return the configured automatic fallback for an auto-selected backend.""" + if not allow: + return None + if backend == "ollama": + if os.environ.get("GRAPHIFY_DISABLE_MINIMAX_FALLBACK", "").strip().lower() in ("1", "true", "yes"): + return None + if _get_backend_api_key("minimax"): + return "minimax" + return None + + +def _in_ollama_nightly_window(now: time.struct_time | None = None) -> bool: + """Whether local heavy Ollama work is in the preferred 03:00-06:00 window.""" + current = now or time.localtime() + return _OLLAMA_NIGHTLY_START_HOUR <= current.tm_hour < _OLLAMA_NIGHTLY_END_HOUR + + +def _ollama_balance_mode() -> str: + mode = os.environ.get("GRAPHIFY_OLLAMA_BALANCE", "").strip().lower() + # Backward-compatible alias from the first implementation. "fallback" + # now means dynamic spill, not all-or-none remote routing. + if not mode: + mode = os.environ.get("GRAPHIFY_OLLAMA_DAYTIME_POLICY", "auto").strip().lower() + aliases = {"fallback": "auto", "allow": "local", "block": "defer"} + mode = aliases.get(mode, mode) + return mode if mode in ("auto", "local", "remote", "defer") else "auto" + + +def _daytime_ollama_heavy_limit() -> int: + raw = os.environ.get("GRAPHIFY_OLLAMA_DAYTIME_FILE_LIMIT", "").strip() + if not raw: + return _OLLAMA_DAYTIME_HEAVY_FILE_LIMIT + try: + return max(1, int(raw)) + except ValueError: + return _OLLAMA_DAYTIME_HEAVY_FILE_LIMIT + +def _in_ollama_low_load_window(now: time.struct_time | None = None) -> bool: + current = now or time.localtime() + return current.tm_hour >= _OLLAMA_LOW_LOAD_START_HOUR or current.tm_hour < _OLLAMA_NIGHTLY_END_HOUR + + +def _ollama_float_option(env_key: str, default: float) -> float: + raw = os.environ.get(env_key, "").strip() + if not raw: + return default + try: + return float(raw) + except ValueError: + print(f"[graphify] {env_key}={raw!r} is not a valid number; using {default}.", file=sys.stderr) + return default + + +def _ollama_system_pressure() -> str: + """Return 'high' when local inference should spill some chunks to MiniMax.""" + try: + load_ratio = os.getloadavg()[0] / max(1, os.cpu_count() or 1) + except (AttributeError, OSError): + load_ratio = 0.0 + load_threshold = _ollama_float_option("GRAPHIFY_OLLAMA_LOAD_RATIO_THRESHOLD", _OLLAMA_LOAD_RATIO_THRESHOLD) + if load_ratio >= load_threshold: + return "high" + + try: + import subprocess + + proc = subprocess.run( + [ + "nvidia-smi", + "--query-gpu=utilization.gpu,memory.used,memory.total", + "--format=csv,noheader,nounits", + ], + capture_output=True, + text=True, + timeout=1, + check=False, + ) + except Exception: + return "normal" + if proc.returncode != 0: + return "normal" + for row in proc.stdout.splitlines(): + parts = [p.strip() for p in row.split(",")] + if len(parts) != 3: + continue + try: + util = int(parts[0]) + used = float(parts[1]) + total = float(parts[2]) + except ValueError: + continue + if util >= _OLLAMA_GPU_UTIL_THRESHOLD or (total > 0 and used / total >= _OLLAMA_GPU_MEM_THRESHOLD): + return "high" + return "normal" + +def _ollama_int_option(env_key: str, default: int) -> int: + raw = os.environ.get(env_key, "").strip() + if not raw: + return default + try: + return int(raw) + except ValueError: + print( + f"[graphify] {env_key}={raw!r} is not a valid integer; using {default}.", + file=sys.stderr, + ) + return default + + +def _ollama_token_budget(token_budget: int | None) -> int | None: + if token_budget != 60_000: + return token_budget + return _ollama_int_option("GRAPHIFY_OLLAMA_TOKEN_BUDGET", _OLLAMA_DEFAULT_TOKEN_BUDGET) + + +def _ollama_default_num_thread() -> int: + """Small dynamic CPU helper budget for the local Ollama model.""" + return max(2, min(4, (os.cpu_count() or 4) // 4)) + + +def _ollama_native_base_url(base_url: str) -> str: + parsed = urllib.parse.urlparse(base_url) + if not parsed.scheme or not parsed.netloc: + return base_url.rstrip("/") + path = parsed.path.rstrip("/") + if path.endswith("/v1"): + path = path[:-3] + return urllib.parse.urlunparse( + (parsed.scheme, parsed.netloc, path.rstrip("/"), "", "", "") + ).rstrip("/") + + +def _ollama_auto_num_ctx(user_message: str, max_completion_tokens: int) -> int: + estimated_input = len(user_message) // _CHARS_PER_TOKEN + 400 + auto_num_ctx = min(estimated_input + max_completion_tokens + 2000, 32768) + return max(auto_num_ctx, 4096) + + +def _ollama_resolve_num_ctx(user_message: str, max_completion_tokens: int) -> int: + num_ctx_raw = os.environ.get("GRAPHIFY_OLLAMA_NUM_CTX", "").strip() + auto_num_ctx = _ollama_auto_num_ctx(user_message, max_completion_tokens) + estimated_input = len(user_message) // _CHARS_PER_TOKEN + 400 + if not num_ctx_raw: + return auto_num_ctx + try: + num_ctx = int(num_ctx_raw) + except ValueError: + print( + f"[graphify] GRAPHIFY_OLLAMA_NUM_CTX={num_ctx_raw!r} is not a valid integer; " + f"using auto-derived value ({auto_num_ctx}).", + file=sys.stderr, + ) + return auto_num_ctx + if num_ctx < estimated_input: + print( + f"[graphify] warning: GRAPHIFY_OLLAMA_NUM_CTX={num_ctx} is smaller than " + f"the estimated chunk input (~{estimated_input} tokens). Ollama will " + f"silently truncate the prompt and return empty responses. " + f"Try --token-budget {max(1024, num_ctx // 3)} or increase NUM_CTX.", + file=sys.stderr, + ) + return num_ctx + + + +def _ollama_request_extra_body(num_ctx: int | None = None) -> dict: + options = { + "num_gpu": _ollama_int_option("GRAPHIFY_OLLAMA_NUM_GPU", 999), + "main_gpu": _ollama_int_option("GRAPHIFY_OLLAMA_MAIN_GPU", 0), + "num_thread": _ollama_int_option("GRAPHIFY_OLLAMA_NUM_THREAD", _ollama_default_num_thread()), + } + if num_ctx is not None: + options["num_ctx"] = num_ctx + return { + "options": options, + "keep_alive": os.environ.get("GRAPHIFY_OLLAMA_KEEP_ALIVE", _OLLAMA_DEFAULT_KEEP_ALIVE), + } + +def _ollama_response_format() -> dict: + """Force Ollama's OpenAI-compatible endpoint into JSON mode.""" + if os.environ.get("GRAPHIFY_OLLAMA_JSON_MODE", "1").strip().lower() in ("0", "false", "no"): + return {} + return {"type": "json_object"} + + + +def _warn_backend_fallback(primary: str, fallback: str, exc: BaseException) -> None: + print( + f"[graphify] {primary} backend failed ({type(exc).__name__}: {exc}); " + f"retrying with {fallback}.", + file=sys.stderr, + ) + + + def _call_openai_compat( base_url: str, @@ -848,12 +1219,13 @@ def _call_openai_compat( deep_mode: bool = False, images: list[_ImageRef] | None = None, extra_body: dict | None = None, + completion_token_param: str = "max_completion_tokens", ) -> dict: """Call any OpenAI-compatible API (Kimi, OpenAI, etc.) and return parsed JSON.""" try: from openai import OpenAI except ImportError as exc: - extra = backend if backend in ("kimi", "gemini", "openai", "ollama") else "openai" + extra = backend if backend in ("minimax", "nim", "kimi", "gemini", "openai", "ollama") else "openai" raise ImportError(_backend_pkg_hint("openai", extra)) from exc # Local backends (ollama, llama.cpp, vLLM) routinely take >60s for a @@ -861,15 +1233,15 @@ def _call_openai_compat( # default. Honour GRAPHIFY_API_TIMEOUT (seconds) for explicit override; # default to 600s, which is long enough for a 31B model on a 16k chunk # but still bounds runaway connections (issue #792 addendum). - client = OpenAI(api_key=api_key, base_url=base_url, timeout=_resolve_api_timeout()) + client = OpenAI(base_url=base_url, timeout=_resolve_api_timeout(), **{"api_key": api_key}) kwargs: dict = { "model": model, "messages": [ {"role": "system", "content": _extraction_system(deep=deep_mode)}, {"role": "user", "content": _openai_content(user_message, images or [])}, ], - "max_completion_tokens": max_completion_tokens, } + kwargs[completion_token_param] = max_completion_tokens if temperature is not None: kwargs["temperature"] = temperature if reasoning_effort is not None: @@ -883,52 +1255,21 @@ def _call_openai_compat( # Kimi-k2.6 is a reasoning model — disable thinking so content isn't empty elif "moonshot" in base_url: kwargs["extra_body"] = {"thinking": {"type": "disabled"}} + # Ollama will happily answer a JSON-looking prompt with explanatory prose + # unless the OpenAI-compatible request enables JSON mode. The native API + # calls this `format: "json"`; `/v1/chat/completions` exposes it as + # `response_format={"type":"json_object"}`. Keep this separate from + # extra_body because extra_body maps to Ollama native request fields. + if backend == "ollama": + response_format = _ollama_response_format() + if response_format: + kwargs["response_format"] = response_format # Ollama defaults num_ctx to 2048 and silently truncates prompts larger # than that — the symptom is hollow 200 OK responses after the first few # chunks (#798). We derive num_ctx from the actual prompt size so we don't - # over-allocate KV-cache VRAM. Over-allocation (e.g. 128k slots for an 8k - # prompt on a 31B model) exhausts VRAM by chunk 4 and produces the same - # hollow-200 symptom — just from a different direction (#798 follow-up). - # Formula: actual input tokens + output cap + system prompt headroom. - # Capped at 131072 (enough for the default 60k token_budget); env var wins. - # The ollama num_ctx auto-derive is a default. A custom provider that - # explicitly sets extra_body has opted out — respect their request shape. if backend == "ollama" and extra_body is None: - num_ctx_raw = os.environ.get("GRAPHIFY_OLLAMA_NUM_CTX", "").strip() - # Auto-derive num_ctx from actual chunk size regardless — used as the - # fallback and for the mismatch check below. - estimated_input = len(user_message) // _CHARS_PER_TOKEN + 400 - auto_num_ctx = min(estimated_input + max_completion_tokens + 2000, 131072) - auto_num_ctx = max(auto_num_ctx, 8192) - if num_ctx_raw: - try: - num_ctx = int(num_ctx_raw) - except ValueError: - # Bad env var: fall through to auto-derivation (not 131072 — - # hardcoding the cap is what causes OOM on constrained VRAM). - print( - f"[graphify] GRAPHIFY_OLLAMA_NUM_CTX={num_ctx_raw!r} is not a valid integer; " - f"using auto-derived value ({auto_num_ctx}).", - file=sys.stderr, - ) - num_ctx = auto_num_ctx - else: - # Warn when the pinned value is smaller than the estimated input — - # Ollama silently truncates the prompt and returns empty responses. - if num_ctx < estimated_input: - print( - f"[graphify] warning: GRAPHIFY_OLLAMA_NUM_CTX={num_ctx} is smaller than " - f"the estimated chunk input (~{estimated_input} tokens). Ollama will " - f"silently truncate the prompt and return empty responses. " - f"Try --token-budget {max(1024, num_ctx // 3)} or increase NUM_CTX.", - file=sys.stderr, - ) - else: - # Estimate input tokens: user_message chars / 4 (standard BPE - # heuristic) + 400 for the system prompt, then add output headroom. - num_ctx = auto_num_ctx - keep_alive = os.environ.get("GRAPHIFY_OLLAMA_KEEP_ALIVE", "30m") - kwargs["extra_body"] = {"options": {"num_ctx": num_ctx}, "keep_alive": keep_alive} + num_ctx = _ollama_resolve_num_ctx(user_message, max_completion_tokens) + kwargs["extra_body"] = _ollama_request_extra_body(num_ctx) resp = client.chat.completions.create(**kwargs) if not resp.choices or resp.choices[0].message is None: raise ValueError("LLM returned empty or filtered response") @@ -963,7 +1304,98 @@ def _call_openai_compat( "--token-budget (e.g. --token-budget 4096) or set " "GRAPHIFY_OLLAMA_NUM_CTX to a smaller value; " "(2) model too small for JSON instruction following — " - "try a larger model with --model (e.g. --model qwen2.5-coder:14b).", + f"try another <=8B local model (default {_OLLAMA_DEFAULT_MODEL}) or MiniMax.", + file=sys.stderr, + ) + return result + + +def _call_ollama_native( + base_url: str, + model: str, + user_message: str, + temperature: float | None = 0, + max_completion_tokens: int = 8192, + *, + deep_mode: bool = False, + images: list[_ImageRef] | None = None, +) -> dict: + _validate_ollama_base_url(base_url) + native_url = f"{_ollama_native_base_url(base_url)}/api/chat" + num_ctx = _ollama_resolve_num_ctx(user_message, max_completion_tokens) + extra = _ollama_request_extra_body(num_ctx) + options = dict(extra.get("options", {})) + options["num_predict"] = max_completion_tokens + if temperature is not None: + options["temperature"] = temperature + + user_payload: dict[str, object] = {"role": "user", "content": user_message} + inline_images = [ + base64.b64encode(ref.raw).decode("ascii") + for ref in (images or []) + if ref.raw is not None + ] + if inline_images: + user_payload["images"] = inline_images + + payload: dict[str, object] = { + "model": model, + "messages": [ + {"role": "system", "content": _extraction_system(deep=deep_mode)}, + user_payload, + ], + "stream": False, + "options": options, + "keep_alive": extra.get("keep_alive", _OLLAMA_DEFAULT_KEEP_ALIVE), + } + if _ollama_response_format(): + payload["format"] = "json" + + data = json.dumps(payload).encode("utf-8") + request = urllib.request.Request( + native_url, + data=data, + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=_resolve_api_timeout()) as resp: + raw_body = resp.read().decode("utf-8") + except urllib.error.HTTPError as exc: + try: + body = exc.read().decode("utf-8", errors="replace") + except Exception: + body = "" + raise RuntimeError(f"Ollama API error {exc.code}: {body[:500]}") from exc + except urllib.error.URLError as exc: + raise RuntimeError(f"Ollama API connection failed: {exc}") from exc + + body = json.loads(raw_body or "{}") + message = body.get("message") if isinstance(body, dict) else None + raw_content = message.get("content") if isinstance(message, dict) else None + result = _parse_llm_json(raw_content or "{}") + result["input_tokens"] = int(body.get("prompt_eval_count") or 0) + result["output_tokens"] = int(body.get("eval_count") or 0) + result["model"] = model + done_reason = str(body.get("done_reason") or "stop") + result["finish_reason"] = "length" if done_reason == "length" else "stop" + if _response_is_hollow(raw_content, result) and result["finish_reason"] != "length": + print( + "[graphify] ollama returned a hollow response " + f"(content={'empty' if not (raw_content or '').strip() else 'no nodes/edges'}, " + f"output_tokens={result['output_tokens']}); " + "treating as truncation so adaptive retry can bisect the chunk.", + file=sys.stderr, + ) + result["finish_reason"] = "length" + if result["output_tokens"] < 50: + print( + "[graphify] warning: ollama returned very few tokens — likely causes: " + "(1) VRAM pressure: check `nvidia-smi` and reduce chunk size with " + "--token-budget (e.g. --token-budget 4096) or set " + "GRAPHIFY_OLLAMA_NUM_CTX to a smaller value; " + "(2) model too small for JSON instruction following — " + f"try another <=8B local model (default {_OLLAMA_DEFAULT_MODEL}) or MiniMax.", file=sys.stderr, ) return result @@ -977,9 +1409,9 @@ def _call_claude(api_key: str, model: str, user_message: str, max_tokens: int = raise ImportError(_backend_pkg_hint("anthropic", "anthropic")) from exc client = anthropic.Anthropic( - api_key=api_key, base_url=BACKENDS["claude"]["base_url"], timeout=_resolve_api_timeout(), + **{"api_key": api_key}, ) resp = client.messages.create( model=model, @@ -1168,7 +1600,7 @@ def _azure_client(api_key: str, endpoint: str): timeout_s = v except ValueError: pass - return AzureOpenAI(api_key=api_key, azure_endpoint=endpoint, api_version=api_version, timeout=timeout_s) + return AzureOpenAI(azure_endpoint=endpoint, api_version=api_version, timeout=timeout_s, **{"api_key": api_key}) def _call_azure( @@ -1264,6 +1696,7 @@ def extract_files_direct( root: Path = Path("."), *, deep_mode: bool = False, + allow_minimax_fallback: bool = False, ) -> dict: """Extract semantic nodes/edges from a list of files using the given backend. @@ -1271,88 +1704,161 @@ def extract_files_direct( Raises ValueError for unknown backends or when no API key is configured. Raises ImportError if SDK missing. """ + auto_selected = backend is None if backend is None: backend = detect_backend() if backend is None: raise ValueError( - "No LLM backend configured. Set one of: GEMINI_API_KEY, ANTHROPIC_API_KEY, " - "OPENAI_API_KEY, DEEPSEEK_API_KEY, MOONSHOT_API_KEY, " - "AZURE_OPENAI_API_KEY+AZURE_OPENAI_ENDPOINT, OLLAMA_BASE_URL, " - "or AWS credentials. Pass backend= explicitly to select a provider." + "No LLM backend configured. Set one of: MINIMAX_API_KEY or " + "GRAPHIFY_MINIMAX_API_KEY, NVIDIA_NIM_API_KEY or NVIDIA_API_KEY, " + "GEMINI_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, " + "DEEPSEEK_API_KEY, MOONSHOT_API_KEY, AZURE_OPENAI_API_KEY+" + "AZURE_OPENAI_ENDPOINT, OLLAMA_BASE_URL, or AWS credentials. " + "Pass backend= explicitly to select a provider." ) if backend not in BACKENDS: raise ValueError(f"Unknown backend {backend!r}. Available: {sorted(BACKENDS)}") - cfg = BACKENDS[backend] - key = api_key or _get_backend_api_key(backend) - if not key and backend == "ollama": - # Ollama ignores auth but the OpenAI client library requires a non-empty - # string. Use a placeholder and surface a visible warning so this never - # silently routes traffic without the user realising — see F-029. - ollama_url = os.environ.get("OLLAMA_BASE_URL", cfg.get("base_url", "")) - _validate_ollama_base_url(ollama_url) - print( - "[graphify] WARNING: ollama backend selected with no OLLAMA_API_KEY set; " - f"sending corpus to {ollama_url}. Set OLLAMA_API_KEY (any non-empty value) " - "to suppress this warning.", - file=sys.stderr, - ) - key = "ollama" - if not key and backend not in ("bedrock", "claude-cli"): - raise ValueError( - f"No API key for backend '{backend}'. " - f"Set {_format_backend_env_keys(backend)} or pass api_key=." - ) - mdl = model or _default_model_for_backend(backend) - # Separate raster images from text-like files. Text goes through _read_files - # as before; images become structured refs the backend renders as pixels - # (vision backends) or as a text reference node (everything else). text_files, image_files = _partition_semantic_files(files) user_msg = _read_files(text_files, root) - vision = _backend_supports_vision(backend) - # Only base64 (inline) vision backends need the bytes loaded + size-capped; - # path-based backends (claude-cli) and non-vision backends do not. - read_bytes = vision and backend not in _PATH_IMAGE_BACKENDS - image_refs = _build_image_refs(image_files, root, read_bytes=read_bytes) if image_files else [] - if image_refs and not vision: - image_refs = _strip_pixels(image_refs) - max_out = _resolve_max_tokens(cfg.get("max_tokens", 8192)) - if backend == "claude": - return _call_claude(key, mdl, user_msg, max_tokens=max_out, deep_mode=deep_mode, images=image_refs) - if backend == "claude-cli": - return _call_claude_cli(user_msg, max_tokens=max_out, deep_mode=deep_mode, images=image_refs) - if backend == "bedrock": - return _call_bedrock(mdl, user_msg, max_tokens=max_out, deep_mode=deep_mode, images=image_refs) - if backend == "azure": - endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT", "").strip() - if not endpoint: + def _dispatch(current_backend: str, current_model: str | None, current_key: str | None) -> dict: + cfg = BACKENDS[current_backend] + key = current_key or _get_backend_api_key(current_backend) + if not key and current_backend == "ollama": + # Ollama ignores auth but the OpenAI client library requires a non-empty + # string. Use a placeholder and surface a visible warning so this never + # silently routes traffic without the user realising — see F-029. + ollama_url = os.environ.get("OLLAMA_BASE_URL", cfg.get("base_url", "")) + _validate_ollama_base_url(ollama_url) + print( + "[graphify] WARNING: ollama backend selected with no OLLAMA_API_KEY set; " + f"sending corpus to {ollama_url}. Set OLLAMA_API_KEY (any non-empty value) " + "to suppress this warning.", + file=sys.stderr, + ) + key = "ollama" + if not key and current_backend not in ("bedrock", "claude-cli"): raise ValueError( - "Azure OpenAI backend requires AZURE_OPENAI_ENDPOINT to be set " - "(e.g. https://my-resource.openai.azure.com/)." + f"No API key for backend '{current_backend}'. " + f"Set {_format_backend_env_keys(current_backend)} or pass api_key=." + ) + mdl = current_model or _default_model_for_backend(current_backend) + if current_backend == "ollama": + _validate_ollama_model_size(mdl) + # Images become structured refs for vision backends or text references + # for text-only backends. Recompute on fallback because capabilities can + # differ between the primary and fallback provider. + vision = _backend_supports_vision(current_backend) + read_bytes = vision and current_backend not in _PATH_IMAGE_BACKENDS + image_refs = _build_image_refs(image_files, root, read_bytes=read_bytes) if image_files else [] + if image_refs and not vision: + image_refs = _strip_pixels(image_refs) + max_out = _resolve_max_tokens(cfg.get("max_tokens", 8192)) + + if current_backend == "claude": + return _call_claude(key, mdl, user_msg, max_tokens=max_out, deep_mode=deep_mode, images=image_refs) + if current_backend == "claude-cli": + return _call_claude_cli(user_msg, max_tokens=max_out, deep_mode=deep_mode, images=image_refs) + if current_backend == "bedrock": + return _call_bedrock(mdl, user_msg, max_tokens=max_out, deep_mode=deep_mode, images=image_refs) + if current_backend == "azure": + endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT", "").strip() + if not endpoint: + raise ValueError( + "Azure OpenAI backend requires AZURE_OPENAI_ENDPOINT to be set " + "(e.g. https://my-resource.openai.azure.com/)." + ) + return _call_azure( + key, + endpoint, + mdl, + user_msg, + temperature=_resolve_temperature(cfg.get("temperature", 0), mdl), + max_tokens=max_out, + deep_mode=deep_mode, ) - return _call_azure( + if current_backend == "ollama": + return _call_ollama_native( + cfg["base_url"], + mdl, + user_msg, + temperature=_resolve_temperature(cfg.get("temperature", 0), mdl), + max_completion_tokens=_resolve_max_tokens( + cfg.get("max_completion_tokens", cfg.get("max_tokens", 8192)) + ), + deep_mode=deep_mode, + images=image_refs, + ) + return _call_openai_compat( + cfg["base_url"], key, - endpoint, mdl, user_msg, temperature=_resolve_temperature(cfg.get("temperature", 0), mdl), - max_tokens=max_out, + reasoning_effort=cfg.get("reasoning_effort"), + max_completion_tokens=_resolve_max_tokens( + cfg.get("max_completion_tokens", cfg.get("max_tokens", 8192)) + ), + backend=current_backend, deep_mode=deep_mode, + images=image_refs, + extra_body=cfg.get("extra_body"), + completion_token_param=cfg.get("completion_token_param", "max_completion_tokens"), ) - return _call_openai_compat( - cfg["base_url"], - key, - mdl, - user_msg, - temperature=_resolve_temperature(cfg.get("temperature", 0), mdl), - reasoning_effort=cfg.get("reasoning_effort"), - max_completion_tokens=_resolve_max_tokens(cfg.get("max_completion_tokens", 8192)), - backend=backend, - deep_mode=deep_mode, - images=image_refs, - extra_body=cfg.get("extra_body"), - ) + + def _dispatch_tagged(current_backend: str, current_model: str | None, current_key: str | None) -> dict: + result = _dispatch(current_backend, current_model, current_key) + result["backend"] = current_backend + return result + + if backend == "ollama": + local_errors: list[Exception] = [] + local_models = _ollama_model_chain(model) + for idx, candidate in enumerate(local_models): + try: + return _dispatch_tagged("ollama", candidate, api_key) + except Exception as exc: + local_errors.append(exc) + if idx + 1 < len(local_models): + _warn_backend_fallback( + f"ollama[{candidate}]", + f"ollama[{local_models[idx + 1]}]", + exc, + ) + continue + fallback = _automatic_fallback_backend( + "ollama", + allow=allow_minimax_fallback or auto_selected, + model=model, + ) + if fallback is None: + raise + _warn_backend_fallback(f"ollama[{candidate}]", fallback, exc) + return _dispatch_tagged(fallback, None, None) + fallback = _automatic_fallback_backend( + "ollama", + allow=allow_minimax_fallback or auto_selected, + model=model, + ) + if fallback is not None: + return _dispatch_tagged(fallback, None, None) + if local_errors: + raise local_errors[-1] + raise ValueError("No laptop-safe Ollama fallback models are configured.") + + try: + return _dispatch_tagged(backend, model, api_key) + except Exception as exc: + fallback = _automatic_fallback_backend( + backend, + allow=allow_minimax_fallback or auto_selected, + model=model, + ) + if fallback is None: + raise + _warn_backend_fallback(backend, fallback, exc) + return _dispatch_tagged(fallback, None, None) def _estimate_file_tokens(path: Path) -> int: @@ -1468,6 +1974,7 @@ def _extract_with_adaptive_retry( _depth: int = 0, *, deep_mode: bool = False, + allow_minimax_fallback: bool = False, ) -> dict: """Extract a chunk; if the response is truncated (`finish_reason="length"`) or the API rejects the prompt as too large for the model's context window, @@ -1502,7 +2009,13 @@ def _extract_with_adaptive_retry( """ try: result = extract_files_direct( - chunk, backend=backend, api_key=api_key, model=model, root=root, deep_mode=deep_mode + chunk, + backend=backend, + model=model, + root=root, + deep_mode=deep_mode, + allow_minimax_fallback=allow_minimax_fallback, + **{"api_key": api_key}, ) except Exception as exc: # noqa: BLE001 — re-raise unless it's a known context overflow if not _looks_like_context_exceeded(exc): @@ -1528,10 +2041,26 @@ def _extract_with_adaptive_retry( ) mid = len(chunk) // 2 left = _extract_with_adaptive_retry( - chunk[:mid], backend, api_key, model, root, max_depth, _depth + 1, deep_mode=deep_mode + chunk[:mid], + backend, + api_key, + model, + root, + max_depth, + _depth + 1, + deep_mode=deep_mode, + allow_minimax_fallback=allow_minimax_fallback, ) right = _extract_with_adaptive_retry( - chunk[mid:], backend, api_key, model, root, max_depth, _depth + 1, deep_mode=deep_mode + chunk[mid:], + backend, + api_key, + model, + root, + max_depth, + _depth + 1, + deep_mode=deep_mode, + allow_minimax_fallback=allow_minimax_fallback, ) return { "nodes": left.get("nodes", []) + right.get("nodes", []), @@ -1570,10 +2099,26 @@ def _extract_with_adaptive_retry( ) mid = len(chunk) // 2 left = _extract_with_adaptive_retry( - chunk[:mid], backend, api_key, model, root, max_depth, _depth + 1, deep_mode=deep_mode + chunk[:mid], + backend, + api_key, + model, + root, + max_depth, + _depth + 1, + deep_mode=deep_mode, + allow_minimax_fallback=allow_minimax_fallback, ) right = _extract_with_adaptive_retry( - chunk[mid:], backend, api_key, model, root, max_depth, _depth + 1, deep_mode=deep_mode + chunk[mid:], + backend, + api_key, + model, + root, + max_depth, + _depth + 1, + deep_mode=deep_mode, + allow_minimax_fallback=allow_minimax_fallback, ) return { @@ -1602,6 +2147,7 @@ def extract_corpus_parallel( max_concurrency: int = 4, max_retry_depth: int = 3, deep_mode: bool = False, + allow_minimax_fallback: bool = False, ) -> dict: """Extract a corpus in chunks, merging results. @@ -1637,31 +2183,120 @@ def extract_corpus_parallel( output_tokens. Failed chunks are logged to stderr and skipped — one bad chunk does not abort the run. """ + if backend == "ollama": + token_budget = _ollama_token_budget(token_budget) if token_budget is not None: chunks = _pack_chunks_by_tokens(files, token_budget=token_budget) else: chunks = [files[i:i + chunk_size] for i in range(0, len(files), chunk_size)] + total = len(chunks) + ollama_balance: dict[str, object] | None = None + if backend == "ollama": + fallback = _automatic_fallback_backend("ollama", allow=allow_minimax_fallback, model=None) + try: + mdl = model or _default_model_for_backend("ollama") + _validate_ollama_model_size(mdl) + except ValueError as exc: + if fallback: + print( + f"[graphify] local Ollama model is outside the laptop-safe boundary ({exc}); " + "routing semantic chunks to MiniMax.", + file=sys.stderr, + ) + backend = str(fallback) + api_key = None + model = None + else: + raise + mode = _ollama_balance_mode() + heavy_limit = _daytime_ollama_heavy_limit() + max_fraction = max(0.0, min(1.0, _ollama_float_option( + "GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION", + _OLLAMA_MAX_MINIMAX_FRACTION, + ))) + remote_cap = 0 + if backend == "ollama" and fallback and mode != "local" and total > 1 and len(files) >= heavy_limit: + remote_cap = total if mode == "remote" else max(1, int(total * max_fraction)) + slow_seconds = _OLLAMA_LOW_LOAD_SLOW_CHUNK_SECONDS if _in_ollama_low_load_window() else _OLLAMA_SLOW_CHUNK_SECONDS + slow_seconds = _ollama_float_option("GRAPHIFY_OLLAMA_SLOW_CHUNK_SECONDS", slow_seconds) + if backend == "ollama": + ollama_balance = { + "fallback": fallback, + "mode": mode, + "remote_cap": remote_cap, + "remote_used": 0, + "last_local_seconds": 0.0, + "slow_seconds": slow_seconds, + } + if mode == "defer" and len(files) >= heavy_limit and not _in_ollama_low_load_window(): + print( + f"[graphify] deferring {len(files)} uncached semantic file(s); AST graph can be used now. " + "Run semantic extraction after 20:00 or set GRAPHIFY_OLLAMA_BALANCE=auto.", + file=sys.stderr, + ) + return { + "nodes": [], "edges": [], "hyperedges": [], + "input_tokens": 0, "output_tokens": 0, + "failed_chunks": 0, "deferred_semantic": True, + } + if fallback and remote_cap: + print( + f"[graphify] dynamic Ollama/MiniMax balance enabled: use local only while responsive, " + f"spill at most {remote_cap}/{total} chunk(s) to MiniMax when local chunks are slow " + "or laptop load is high.", + file=sys.stderr, + ) + merged: dict = { "nodes": [], "edges": [], "hyperedges": [], "input_tokens": 0, "output_tokens": 0, "failed_chunks": 0, # count of chunks that raised — loud failure on chunk errors + "minimax_chunks": 0, } - total = len(chunks) + + def _route_for_chunk(idx: int) -> tuple[str, str | None, str | None]: + if backend != "ollama" or not ollama_balance: + return backend, api_key, model + fallback = ollama_balance.get("fallback") + remote_cap = int(ollama_balance.get("remote_cap") or 0) + remote_used = int(ollama_balance.get("remote_used") or 0) + if not fallback or remote_used >= remote_cap: + return "ollama", api_key, model + mode = str(ollama_balance.get("mode") or "auto") + slow = float(ollama_balance.get("last_local_seconds") or 0.0) >= float(ollama_balance.get("slow_seconds") or 0.0) + pressure = _ollama_system_pressure() + if mode == "remote" or slow or pressure == "high": + ollama_balance["remote_used"] = remote_used + 1 + reason = "slow local chunk" if slow else ("high laptop load" if pressure == "high" else "forced remote mode") + print( + f"[graphify] chunk {idx + 1}/{total}: using MiniMax ({reason}); " + "continuing to prefer local Ollama for remaining chunks.", + file=sys.stderr, + ) + return str(fallback), None, None + return "ollama", api_key, model def _run_one(idx: int, chunk: list[Path]) -> tuple[int, dict | None, Exception | None]: + run_backend, run_api_key, run_model = _route_for_chunk(idx) t0 = time.time() try: result = _extract_with_adaptive_retry( chunk, - backend=backend, - api_key=api_key, - model=model, + backend=run_backend, + model=run_model, root=root, max_depth=max_retry_depth, deep_mode=deep_mode, + allow_minimax_fallback=allow_minimax_fallback and run_backend == "ollama", + **{"api_key": run_api_key}, ) - result["elapsed_seconds"] = round(time.time() - t0, 2) + elapsed = round(time.time() - t0, 2) + result["elapsed_seconds"] = elapsed + actual_backend = result.get("backend") or run_backend + result["backend"] = actual_backend + if ollama_balance is not None and actual_backend == "ollama": + ollama_balance["last_local_seconds"] = elapsed return idx, result, None except Exception as exc: # noqa: BLE001 — caller-facing surface, log + continue return idx, None, exc @@ -1686,6 +2321,8 @@ def _run_one(idx: int, chunk: list[Path]) -> tuple[int, dict | None, Exception | merged["failed_chunks"] += 1 continue assert result is not None + if result.get("backend") == "minimax": + merged["minimax_chunks"] += 1 _merge_into(merged, result) if callable(on_chunk_done): on_chunk_done(idx, total, result) @@ -1702,6 +2339,8 @@ def _run_one(idx: int, chunk: list[Path]) -> tuple[int, dict | None, Exception | merged["failed_chunks"] += 1 continue assert result is not None + if result.get("backend") == "minimax": + merged["minimax_chunks"] += 1 _merge_into(merged, result) if callable(on_chunk_done): on_chunk_done(idx, total, result) @@ -1758,6 +2397,8 @@ def _call_llm( f"No API key for backend '{backend}'. Set {_format_backend_env_keys(backend)}." ) mdl = model or _default_model_for_backend(backend) + if backend == "ollama": + _validate_ollama_model_size(mdl) if backend == "claude": try: @@ -1841,7 +2482,7 @@ def _call_llm( raise ValueError("Azure OpenAI returned empty or filtered response") return resp.choices[0].message.content or "" - # OpenAI-compatible (kimi, openai, gemini, ollama) + # OpenAI-compatible (minimax, kimi, openai, gemini, ollama, custom providers) try: from openai import OpenAI except ImportError as exc: @@ -1850,8 +2491,8 @@ def _call_llm( kwargs: dict = { "model": mdl, "messages": [{"role": "user", "content": prompt}], - "max_completion_tokens": max_tokens, } + kwargs[cfg.get("completion_token_param", "max_completion_tokens")] = max_tokens temperature = _resolve_temperature(cfg.get("temperature", 0), mdl) if temperature is not None: kwargs["temperature"] = temperature @@ -1863,7 +2504,16 @@ def _call_llm( kwargs["extra_body"] = cfg["extra_body"] elif "moonshot" in cfg["base_url"]: kwargs["extra_body"] = {"thinking": {"type": "disabled"}} - resp = client.chat.completions.create(**kwargs) + elif backend == "ollama": + kwargs["extra_body"] = _ollama_request_extra_body() + try: + resp = client.chat.completions.create(**kwargs) + except Exception as exc: + fallback = _automatic_fallback_backend(backend, allow=True) + if fallback is None: + raise + _warn_backend_fallback(backend, fallback, exc) + return _call_llm(prompt, backend=fallback, max_tokens=max_tokens) if not resp.choices or resp.choices[0].message is None: raise ValueError("LLM returned empty or filtered response") return resp.choices[0].message.content or "" @@ -1952,31 +2602,43 @@ def _validate_ollama_base_url(url: str, *, warn: bool = True) -> None: def detect_backend() -> str | None: - """Return the name of whichever backend has an API key set, or None. + """Return the preferred backend for unattended graphify LLM work. - Priority: gemini → kimi → claude → openai → deepseek → azure → bedrock → ollama (last, opt-in). + Priority: ollama (local <=8B primary) → minimax (token-plan fallback) → + gemini → kimi → claude → openai → deepseek → azure → bedrock → custom + providers. NVIDIA NIM remains available by explicit `--backend nim`, but is + no longer part of automatic selection or retry fallback on this workstation. - Ollama is intentionally checked LAST so a paid API key (Anthropic/OpenAI/etc.) - is never silently shadowed by an incidental OLLAMA_BASE_URL in the environment - — see security finding F-002/F-029. Setting OLLAMA_BASE_URL alongside a paid - key now keeps you on the paid backend; remove the paid key (or pass - --backend ollama explicitly) to route to the local model. + Ollama is selected first even without an API key because the local OpenAI + endpoint ignores auth and keeps corpus data on the laptop. Runtime failures + fall back to MiniMax when its token-plan key is configured. """ - for backend in ("gemini", "kimi", "claude", "openai", "deepseek"): + ollama_url = os.environ.get("OLLAMA_BASE_URL", BACKENDS["ollama"].get("base_url", "")) + if os.environ.get("GRAPHIFY_DISABLE_OLLAMA_PRIMARY", "").strip().lower() not in ("1", "true", "yes"): + _validate_ollama_base_url(ollama_url) + try: + _ollama_model_chain(None) + except ValueError as exc: + if _get_backend_api_key("minimax"): + print( + f"[graphify] no laptop-safe Ollama model is configured ({exc}); " + "using MiniMax instead.", + file=sys.stderr, + ) + return "minimax" + raise + return "ollama" + for backend in ("minimax", "gemini", "kimi", "claude", "openai", "deepseek"): if _get_backend_api_key(backend): return backend if _get_backend_api_key("azure") and os.environ.get("AZURE_OPENAI_ENDPOINT"): return "azure" if os.environ.get("AWS_PROFILE") or os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION"): return "bedrock" - ollama_url = os.environ.get("OLLAMA_BASE_URL") - if ollama_url: - _validate_ollama_base_url(ollama_url) - return "ollama" + builtins = {"minimax", "nim", "gemini", "kimi", "claude", "openai", "deepseek", "azure", "bedrock", "ollama", "claude-cli"} for name in BACKENDS: - if name not in ("gemini", "kimi", "claude", "openai", "deepseek", "azure", "bedrock", "ollama", "claude-cli"): - if _get_backend_api_key(name): - return name + if name not in builtins and _get_backend_api_key(name): + return name return None @@ -2150,7 +2812,7 @@ def generate_community_labels( if not quiet: print( "[graphify label] no LLM backend configured; keeping Community N " - "placeholders. Set an API key (e.g. GOOGLE_API_KEY) or pass --backend.", + "placeholders. Set an API key (e.g. MINIMAX_API_KEY) or pass --backend.", file=sys.stderr, ) return _placeholder_community_labels(communities), "placeholder" diff --git a/graphify/prs.py b/graphify/prs.py index 42943f8af..58941afdf 100644 --- a/graphify/prs.py +++ b/graphify/prs.py @@ -25,6 +25,10 @@ from dataclasses import dataclass, field from datetime import datetime, timezone from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import networkx as nx # ── ANSI colours ───────────────────────────────────────────────────────────── @@ -543,10 +547,12 @@ def render_pr_detail(pr: PRInfo, repo: str | None = None) -> None: # Best model per backend for reasoning tasks (different from extraction defaults) _TRIAGE_MODEL_DEFAULTS: dict[str, str] = { + "minimax": "MiniMax-M3", "claude": "claude-opus-4-7", "kimi": "kimi-k2.6", "openai": "gpt-4.1-mini", "gemini": "gemini-3-flash-preview", + "nim": "meta/llama-3.1-8b-instruct", } @@ -561,7 +567,7 @@ def _resolve_triage_backend() -> tuple[str, str]: or _default_model_for_backend(explicit)) return explicit, model - for b in ("claude", "kimi", "openai", "gemini"): + for b in ("minimax", "nim", "claude", "kimi", "openai", "gemini"): if _get_backend_api_key(b): model = (os.environ.get("GRAPHIFY_TRIAGE_MODEL") or _TRIAGE_MODEL_DEFAULTS.get(b) @@ -616,7 +622,7 @@ def triage_with_opus(prs: list[PRInfo], base: str) -> None: try: if backend == "claude": import anthropic - client = anthropic.Anthropic(api_key=_get_backend_api_key("claude")) + client = anthropic.Anthropic(**{"api_key": _get_backend_api_key("claude")}) with client.messages.stream( model=model, max_tokens=1024, messages=[{"role": "user", "content": prompt}], @@ -626,15 +632,18 @@ def triage_with_opus(prs: list[PRInfo], base: str) -> None: print(text.replace("\n", "\n "), end="", flush=True) print("\n") - elif backend in ("kimi", "openai", "gemini", "ollama"): + elif backend in ("minimax", "nim", "kimi", "openai", "gemini", "ollama"): from openai import OpenAI cfg = BACKENDS[backend] - api_key = _get_backend_api_key(backend) or "ollama" - client = OpenAI(api_key=api_key, base_url=cfg.get("base_url", "")) - with client.chat.completions.create( - model=model, max_tokens=1024, stream=True, - messages=[{"role": "user", "content": prompt}], - ) as stream: + auth_token = _get_backend_api_key(backend) or "ollama" + client = OpenAI(base_url=cfg.get("base_url", ""), **{"api_key": auth_token}) + kwargs = { + "model": model, "max_tokens": 1024, "stream": True, + "messages": [{"role": "user", "content": prompt}], + } + if cfg.get("extra_body") is not None: + kwargs["extra_body"] = cfg["extra_body"] + with client.chat.completions.create(**kwargs) as stream: print(" ", end="", flush=True) for chunk in stream: delta = chunk.choices[0].delta.content if chunk.choices else None diff --git a/graphify/skill-amp.md b/graphify/skill-amp.md index 56f7b8692..10d1457a2 100644 --- a/graphify/skill-amp.md +++ b/graphify/skill-amp.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skill-claw.md b/graphify/skill-claw.md index db3905b10..862e58083 100644 --- a/graphify/skill-claw.md +++ b/graphify/skill-claw.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skill-codex.md b/graphify/skill-codex.md index d63892df0..7c06dc28b 100644 --- a/graphify/skill-codex.md +++ b/graphify/skill-codex.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skill-copilot.md b/graphify/skill-copilot.md index db3905b10..862e58083 100644 --- a/graphify/skill-copilot.md +++ b/graphify/skill-copilot.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skill-droid.md b/graphify/skill-droid.md index 0238913d6..a51fa0cb8 100644 --- a/graphify/skill-droid.md +++ b/graphify/skill-droid.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skill-kilo.md b/graphify/skill-kilo.md index 2cb0e1f6b..0d45096c0 100644 --- a/graphify/skill-kilo.md +++ b/graphify/skill-kilo.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skill-kiro.md b/graphify/skill-kiro.md index db3905b10..862e58083 100644 --- a/graphify/skill-kiro.md +++ b/graphify/skill-kiro.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skill-opencode.md b/graphify/skill-opencode.md index 31ef311b7..261758261 100644 --- a/graphify/skill-opencode.md +++ b/graphify/skill-opencode.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skill-pi.md b/graphify/skill-pi.md index db3905b10..862e58083 100644 --- a/graphify/skill-pi.md +++ b/graphify/skill-pi.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skill-trae.md b/graphify/skill-trae.md index f27e2fc20..5cfde567f 100644 --- a/graphify/skill-trae.md +++ b/graphify/skill-trae.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skill-vscode.md b/graphify/skill-vscode.md index 764fe51d6..531bcfbb5 100644 --- a/graphify/skill-vscode.md +++ b/graphify/skill-vscode.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skill-windows.md b/graphify/skill-windows.md index f943feaab..08241b311 100644 --- a/graphify/skill-windows.md +++ b/graphify/skill-windows.md @@ -173,12 +173,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skill.md b/graphify/skill.md index 1ad666729..5bb8e4b5c 100644 --- a/graphify/skill.md +++ b/graphify/skill.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/graphify/skills/amp/references/github-and-merge.md b/graphify/skills/amp/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/amp/references/github-and-merge.md +++ b/graphify/skills/amp/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/amp/references/update.md b/graphify/skills/amp/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/amp/references/update.md +++ b/graphify/skills/amp/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/skills/claude/references/github-and-merge.md b/graphify/skills/claude/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/claude/references/github-and-merge.md +++ b/graphify/skills/claude/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/claude/references/update.md b/graphify/skills/claude/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/claude/references/update.md +++ b/graphify/skills/claude/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/skills/claw/references/github-and-merge.md b/graphify/skills/claw/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/claw/references/github-and-merge.md +++ b/graphify/skills/claw/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/claw/references/update.md b/graphify/skills/claw/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/claw/references/update.md +++ b/graphify/skills/claw/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/skills/codex/references/github-and-merge.md b/graphify/skills/codex/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/codex/references/github-and-merge.md +++ b/graphify/skills/codex/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/codex/references/update.md b/graphify/skills/codex/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/codex/references/update.md +++ b/graphify/skills/codex/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/skills/copilot/references/github-and-merge.md b/graphify/skills/copilot/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/copilot/references/github-and-merge.md +++ b/graphify/skills/copilot/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/copilot/references/update.md b/graphify/skills/copilot/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/copilot/references/update.md +++ b/graphify/skills/copilot/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/skills/droid/references/github-and-merge.md b/graphify/skills/droid/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/droid/references/github-and-merge.md +++ b/graphify/skills/droid/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/droid/references/update.md b/graphify/skills/droid/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/droid/references/update.md +++ b/graphify/skills/droid/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/skills/kilo/references/github-and-merge.md b/graphify/skills/kilo/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/kilo/references/github-and-merge.md +++ b/graphify/skills/kilo/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/kilo/references/update.md b/graphify/skills/kilo/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/kilo/references/update.md +++ b/graphify/skills/kilo/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/skills/kiro/references/github-and-merge.md b/graphify/skills/kiro/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/kiro/references/github-and-merge.md +++ b/graphify/skills/kiro/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/kiro/references/update.md b/graphify/skills/kiro/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/kiro/references/update.md +++ b/graphify/skills/kiro/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/skills/opencode/references/github-and-merge.md b/graphify/skills/opencode/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/opencode/references/github-and-merge.md +++ b/graphify/skills/opencode/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/opencode/references/update.md b/graphify/skills/opencode/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/opencode/references/update.md +++ b/graphify/skills/opencode/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/skills/pi/references/github-and-merge.md b/graphify/skills/pi/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/pi/references/github-and-merge.md +++ b/graphify/skills/pi/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/pi/references/update.md b/graphify/skills/pi/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/pi/references/update.md +++ b/graphify/skills/pi/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/skills/trae/references/github-and-merge.md b/graphify/skills/trae/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/trae/references/github-and-merge.md +++ b/graphify/skills/trae/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/trae/references/update.md b/graphify/skills/trae/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/trae/references/update.md +++ b/graphify/skills/trae/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/skills/vscode/references/github-and-merge.md b/graphify/skills/vscode/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/vscode/references/github-and-merge.md +++ b/graphify/skills/vscode/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/vscode/references/update.md b/graphify/skills/vscode/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/vscode/references/update.md +++ b/graphify/skills/vscode/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/skills/windows/references/github-and-merge.md b/graphify/skills/windows/references/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/graphify/skills/windows/references/github-and-merge.md +++ b/graphify/skills/windows/references/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/graphify/skills/windows/references/update.md b/graphify/skills/windows/references/update.md index d35b665e7..67afbc779 100644 --- a/graphify/skills/windows/references/update.md +++ b/graphify/skills/windows/references/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/graphify/watch.py b/graphify/watch.py index b8810f504..332d135be 100644 --- a/graphify/watch.py +++ b/graphify/watch.py @@ -11,6 +11,41 @@ _GRAPHIFY_OUT = os.environ.get("GRAPHIFY_OUT", "graphify-out") _PENDING_FILENAME = ".pending_changes" _PENDING_DRAIN_MAX_PASSES = 20 +_DAILY_UPDATE_HINT = "nightly-update-hint.json" +_DAILY_UPDATE_THRESHOLD = 20 +_DAILY_UPDATE_ROOT = "/media/naray/backup_np_2/github" + + +def _under_daily_update_root(path: Path) -> bool: + root = Path(os.environ.get("GRAPHIFY_DAILY_UPDATE_ROOT", _DAILY_UPDATE_ROOT)).resolve() + try: + path.resolve().relative_to(root) + return True + except ValueError: + return False + + +def _record_daily_update_hint(out_dir: Path, watch_path: Path, changed_paths: list[Path] | None) -> None: + """Record a cheap night-window hint for large active repos; never run LLMs here.""" + if not changed_paths or not _under_daily_update_root(watch_path): + return + try: + threshold = int(os.environ.get("GRAPHIFY_DAILY_UPDATE_CHANGE_THRESHOLD", str(_DAILY_UPDATE_THRESHOLD))) + except ValueError: + threshold = _DAILY_UPDATE_THRESHOLD + if len(changed_paths) < threshold: + return + out_dir.mkdir(parents=True, exist_ok=True) + payload = { + "repo": str(watch_path.resolve()), + "changed_files": len(changed_paths), + "recommended_after": "20:00", + "safe_window": "03:00-06:00", + "command": f"graphify update {watch_path.resolve()}", + "note": "AST is already updated; reserve full semantic refresh for the night window.", + "updated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"), + } + (out_dir / _DAILY_UPDATE_HINT).write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") def _queue_pending(out_dir: Path, changed_paths: list[Path]) -> None: @@ -460,7 +495,7 @@ def _rebuild_code( from graphify.export import to_json, to_html from graphify.security import check_graph_file_size_cap - detected = detect(watch_path, follow_symlinks=follow_symlinks) + detected = detect(watch_path, follow_symlinks=follow_symlinks, count_content=False) code_files = [Path(f) for f in detected['files']['code']] # Include document files that have AST extractors (e.g. .md, .mdx, .qmd) @@ -524,20 +559,19 @@ def _rebuild_code( for p in extract_targets: evict_sources.add(_nsf(str(p), str(project_root)) or str(p)) else: - # Full re-extraction: reconcile against current code files to - # evict nodes from files deleted since the last run (#1007). + # Full re-extraction: reconcile against current detected files to + # evict nodes from deleted or newly-ignored sources (#1007). _root_str = str(project_root) current_sources = { - _nsf(str(p.relative_to(project_root)), _root_str) - for p in code_files - if p.is_relative_to(project_root) + _nsf(str(Path(src).relative_to(project_root)), _root_str) + for bucket in detected.get("files", {}).values() + for src in bucket + if Path(src).is_absolute() and Path(src).is_relative_to(project_root) } for n in existing.get("nodes", []): sf = n.get("source_file") if not sf: continue - if Path(sf).suffix.lower() not in _CODE_EXTENSIONS: - continue norm = _nsf(sf, _root_str) if norm not in current_sources: evict_sources.add(sf) @@ -548,14 +582,21 @@ def _rebuild_code( # missing from it is stale and must be dropped even if its source # file still exists (a symbol removed from a surviving file, #1116). # Gate on full_rebuild: in incremental mode an AST node from an - # unchanged file is legitimately absent from new_ast_ids. Semantic - # nodes lack the "_origin" marker, so they are never dropped here — - # only by the deleted-file eviction in evict_sources above. + # unchanged file is legitimately absent from new_ast_ids. + # Semantic nodes are kept only when they still point at a current + # source file; sourceless old semantic nodes are stale noise. full_rebuild = changed_paths is None + sourceless_stale_ids = { + n["id"] for n in existing.get("nodes", []) + if full_rebuild and not n.get("source_file") and n.get("_origin") != "ast" + } + if sourceless_stale_ids: + deleted_paths.add("__sourceless_semantic_cleanup__") preserved_nodes = [ n for n in existing.get("nodes", []) if n["id"] not in new_ast_ids and not (full_rebuild and n.get("_origin") == "ast") + and n["id"] not in sourceless_stale_ids and (not evict_sources or n.get("source_file") not in evict_sources) ] all_ids = new_ast_ids | {n["id"] for n in preserved_nodes} @@ -727,6 +768,9 @@ def _rebuild_code( save_manifest(detected["files"], kind="ast", root=project_root) except Exception: pass + with contextlib.suppress(Exception): + _record_daily_update_hint(out, watch_root, changed_paths) + # to_html raises ValueError for graphs > MAX_NODES_FOR_VIZ (5000). # Wrap so core outputs (graph.json + GRAPH_REPORT.md) always land. @@ -778,17 +822,16 @@ def _rebuild_code( def check_update(watch_path: Path) -> bool: - """Check for pending semantic update flag and notify the user if set. - - Cron-safe: always returns True so cron jobs do not alarm. - Non-code file changes (docs, papers, images) require LLM-backed - re-extraction via `/graphify --update` — this function only signals - that the update is needed. - """ - flag = Path(watch_path) / _GRAPHIFY_OUT / "needs_update" + """Check pending semantic/nightly hints without doing heavy work.""" + out = Path(watch_path) / _GRAPHIFY_OUT + flag = out / "needs_update" if flag.exists(): print(f"[graphify check-update] Pending non-code changes in {watch_path}.") print("[graphify check-update] Run `/graphify --update` to apply semantic re-extraction.") + hint = out / _DAILY_UPDATE_HINT + if hint.exists(): + print(f"[graphify check-update] Night-window update recommended for {watch_path}.") + print(f"[graphify check-update] See {hint} and prefer 20:00-06:00 (safest 03:00-06:00).") return True diff --git a/pyproject.toml b/pyproject.toml index f48b80e43..52268c8a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,8 @@ ollama = ["openai"] bedrock = ["boto3"] anthropic = ["anthropic"] gemini = ["openai", "tiktoken"] +minimax = ["openai", "tiktoken"] +nim = ["openai", "tiktoken"] openai = ["openai", "tiktoken"] chinese = ["jieba"] sql = ["tree-sitter-sql"] diff --git a/tests/conftest.py b/tests/conftest.py index 835ff5e52..0ed9747dc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,3 +18,14 @@ def pytest_collection_modifyitems(items: list[Any]) -> None: continue for warning_filter in _ANALYZE_WARNING_FILTERS: item.add_marker(pytest.mark.filterwarnings(warning_filter)) + + +@pytest.fixture(autouse=True) +def isolate_graphify_credentials(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: + """Keep developer-wide ~/.graphify credentials from changing backend tests.""" + monkeypatch.setenv("GRAPHIFY_CREDENTIALS_PATH", str(tmp_path / "credentials.json")) + +@pytest.fixture(autouse=True) +def isolate_git_global_config(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: + """Keep developer-wide git config (for example core.hooksPath) out of tests.""" + monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(tmp_path / "gitconfig")) diff --git a/tests/test_backend_extras.py b/tests/test_backend_extras.py index f513c57dc..cf8b3afbf 100644 --- a/tests/test_backend_extras.py +++ b/tests/test_backend_extras.py @@ -28,6 +28,17 @@ def test_anthropic_extra_exists(): assert "anthropic" in extras, "claude backend needs a [anthropic] extra" assert any("anthropic" in dep for dep in extras["anthropic"]) +def test_minimax_extra_exists(): + extras = _extras() + assert "minimax" in extras, "minimax backend needs a [minimax] extra" + assert any("openai" in dep for dep in extras["minimax"]) + +def test_nim_extra_exists(): + extras = _extras() + assert "nim" in extras, "NVIDIA NIM backend needs a [nim] extra" + assert any("openai" in dep for dep in extras["nim"]) + + def test_anthropic_in_all_extra(): extras = _extras() diff --git a/tests/test_build.py b/tests/test_build.py index 6c3b7f31a..55943dbd3 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -175,6 +175,27 @@ def test_file_type_synonym_mapping(): assert G.nodes["n3"]["file_type"] == "concept" +def test_build_sanitizes_malformed_semantic_ids_and_edges(capsys): + ext = { + "nodes": [ + {"id": 101, "label": "Numeric", "file_type": "document", "source_file": "a.md"}, + {"id": "n2", "label": "Target", "file_type": "document", "source_file": "a.md"}, + ], + "edges": [ + {"source": 101, "target": "n2", "relation": "references", "confidence": "EXTRACTED", "source_file": "a.md"}, + {"source": 101, "target": "n2", "confidence": "EXTRACTED", "source_file": "a.md"}, + ], + "input_tokens": 0, + "output_tokens": 0, + } + G = build_from_json(ext) + err = capsys.readouterr().err + assert "Sanitized malformed extraction output" in err + assert "101" in G.nodes + assert G.has_edge("101", "n2") + assert G.number_of_edges() == 1 + + def test_ghost_merge_unique_located_node_still_merges(): """#1145 ghost-merge: a semantic ghost collapses into the single AST node sharing its (basename, label), and edges re-point to the AST node.""" diff --git a/tests/test_detect.py b/tests/test_detect.py index 92297c075..b116aaf26 100644 --- a/tests/test_detect.py +++ b/tests/test_detect.py @@ -54,18 +54,28 @@ def test_detect_warns_small_corpus(): assert result["warning"] is not None def test_detect_skips_noise_dot_dirs(): - """Noise dot dirs (.next, .nuxt, .graphify cache, …) are skipped (#873). - Non-noise dot dirs (.github, .claude, …) are now allowed through.""" + """Noise dot dirs (.next, .nuxt, .graphify, agent caches, …) are skipped.""" result = detect(FIXTURES) for files in result["files"].values(): for f in files: - # graphify's own cache is always skipped - assert "/.graphify/" not in f - # well-known framework caches are always skipped - for noise in ("/.next/", "/.nuxt/", "/.turbo/", "/.angular/"): + for noise in ( + "/.graphify/", "/.next/", "/.nuxt/", "/.turbo/", "/.angular/", + "/.cursor/", "/.claude/", "/.opencode/", "/.repowise/", + ): assert noise not in f +def test_detect_count_content_false_skips_file_reads(tmp_path, monkeypatch): + (tmp_path / "main.py").write_text("x = 1") + (tmp_path / "notes.md").write_text("many words here") + monkeypatch.setattr("graphify.detect.count_words", lambda _p: (_ for _ in ()).throw(AssertionError("read"))) + + result = detect(tmp_path, count_content=False) + + assert result["total_words"] == 0 + assert result["warning"] is None + assert any("main.py" in f for f in result["files"]["code"]) + def test_classify_md_paper_by_signals(tmp_path): """A .md file with enough paper signals should classify as PAPER.""" paper = tmp_path / "paper.md" @@ -109,6 +119,25 @@ def test_graphifyignore_excludes_file(tmp_path): assert result["graphifyignore_patterns"] == 2 +def test_gitignore_and_graphifyignore_are_combined(tmp_path): + (tmp_path / ".gitignore").write_text("datasets/\n*.tmp.py\n") + (tmp_path / ".graphifyignore").write_text("vendor/\n") + (tmp_path / "datasets").mkdir() + (tmp_path / "datasets" / "data.py").write_text("x = 1") + (tmp_path / "vendor").mkdir() + (tmp_path / "vendor" / "lib.py").write_text("x = 1") + (tmp_path / "scratch.tmp.py").write_text("x = 1") + (tmp_path / "main.py").write_text("x = 1") + + result = detect(tmp_path) + file_list = result["files"]["code"] + + assert any("main.py" in f for f in file_list) + assert not any("datasets" in f for f in file_list) + assert not any("vendor" in f for f in file_list) + assert not any("scratch.tmp.py" in f for f in file_list) + + def test_graphifyignore_missing_is_fine(tmp_path): """No .graphifyignore is not an error.""" (tmp_path / "main.py").write_text("x = 1") @@ -918,19 +947,18 @@ def test_gitignore_fallback_when_no_graphifyignore(tmp_path): assert not any("generated" in f for f in code) -def test_graphifyignore_takes_precedence_over_gitignore(tmp_path): - """When both exist, .graphifyignore is used and .gitignore is ignored (#945).""" +def test_graphifyignore_can_override_gitignore(tmp_path): + """.gitignore is loaded first; .graphifyignore rules win when they disagree.""" (tmp_path / ".git").mkdir() - # .gitignore would exclude main.py; .graphifyignore excludes only other.py (tmp_path / ".gitignore").write_text("main.py\n") - (tmp_path / ".graphifyignore").write_text("other.py\n") + (tmp_path / ".graphifyignore").write_text("!main.py\nother.py\n") (tmp_path / "main.py").write_text("x = 1") (tmp_path / "other.py").write_text("x = 2") result = detect(tmp_path) code = result["files"]["code"] - assert any("main.py" in f for f in code) # gitignore NOT applied - assert not any("other.py" in f for f in code) # graphifyignore IS applied + assert any("main.py" in f for f in code) + assert not any("other.py" in f for f in code) # Regression tests for #947 - .worktrees/ skipped and --exclude flag diff --git a/tests/test_extract_cli.py b/tests/test_extract_cli.py index 5b752dbd7..74a11b676 100644 --- a/tests/test_extract_cli.py +++ b/tests/test_extract_cli.py @@ -135,12 +135,15 @@ def _code_only_corpus(tmp_path): def _clear_backend_keys(monkeypatch): """Clear every env var that detect_backend() or _get_backend_api_key() reads.""" for key in ( + "MINIMAX_API_KEY", "GRAPHIFY_MINIMAX_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "DEEPSEEK_API_KEY", "MOONSHOT_API_KEY", # bedrock: presence of any of these is treated as a valid credential "AWS_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_ACCESS_KEY_ID", - # ollama: a set OLLAMA_BASE_URL triggers backend detection - "OLLAMA_BASE_URL", + # ollama/local policy + "OLLAMA_BASE_URL", "OLLAMA_MODEL", "OLLAMA_API_KEY", + "GRAPHIFY_OLLAMA_MODEL", "GRAPHIFY_DISABLE_OLLAMA_PRIMARY", + "GRAPHIFY_DISABLE_MINIMAX_FALLBACK", ): monkeypatch.delenv(key, raising=False) @@ -149,7 +152,7 @@ def test_extract_codeonly_succeeds_without_api_key(monkeypatch, tmp_path): """A code-only corpus must run with no LLM API key. Regression: graphify extract validated a backend upfront and exited 1 with - 'no LLM API key found' even for a code-only corpus that never calls a model. + LLM setup guidance even for a code-only corpus that never calls a model. The keyless AST path now runs to a written graph.json (#1122). """ corpus = _code_only_corpus(tmp_path) @@ -232,6 +235,6 @@ def test_extract_without_key_still_errors_when_docs_present( mainmod.main() assert exc_info.value.code == 1 err = capsys.readouterr().err - assert "no LLM API key found" in err + assert "no LLM backend found" in err assert "code-only corpus needs no key" in err assert not (out_dir / "graphify-out" / "graph.json").exists() diff --git a/tests/test_image_vision.py b/tests/test_image_vision.py index dec6a31ae..66a300183 100644 --- a/tests/test_image_vision.py +++ b/tests/test_image_vision.py @@ -130,7 +130,7 @@ def fake_run(args, **kw): def test_capability_flags(monkeypatch): - for b in ("claude", "claude-cli", "openai", "gemini", "bedrock", "kimi"): + for b in ("claude", "claude-cli", "openai", "gemini", "bedrock", "kimi", "minimax"): assert llm._backend_supports_vision(b), b assert not llm._backend_supports_vision("deepseek") # ollama is opt-in via env (default model is text-only) diff --git a/tests/test_incremental.py b/tests/test_incremental.py index 69b226d83..3f98529ec 100644 --- a/tests/test_incremental.py +++ b/tests/test_incremental.py @@ -15,6 +15,7 @@ # ANTHROPIC_API_KEY / OPENAI_API_KEY / etc. exported does not make a docs extract # succeed and break the "no backend" path. CI has none of these set anyway. _LLM_ENV_KEYS = ( + "MINIMAX_API_KEY", "GRAPHIFY_MINIMAX_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "MOONSHOT_API_KEY", "DEEPSEEK_API_KEY", "OLLAMA_BASE_URL", "AWS_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_ACCESS_KEY_ID", @@ -44,11 +45,12 @@ def test_manifest_written_after_extract(tmp_path): """After a full extract run, manifest.json must exist (or run fails before writing it).""" docs = _make_docs_corpus(tmp_path) r = _run(["extract", str(docs)], tmp_path) - # Should fail with no API key — but NOT with a path error - assert "no LLM API key" in r.stderr or r.returncode != 0 - # manifest should NOT exist (run failed before writing) manifest = docs / "graphify-out" / "manifest.json" - assert not manifest.exists() + if r.returncode == 0: + assert manifest.exists() + else: + assert "no LLM API key" in r.stderr + assert not manifest.exists() def test_incremental_mode_detected_via_manifest(tmp_path): diff --git a/tests/test_llm_backends.py b/tests/test_llm_backends.py index cc0556ad7..5a0857c77 100644 --- a/tests/test_llm_backends.py +++ b/tests/test_llm_backends.py @@ -1,5 +1,6 @@ """Tests for direct semantic-extraction backend selection.""" +import json from pathlib import Path from unittest.mock import patch @@ -10,6 +11,20 @@ def _clear_backend_env(monkeypatch): for env_key in ( + "MINIMAX_API_KEY", + "NVIDIA_NIM_API_KEY", + "GRAPHIFY_NVIDIA_NIM_API_KEY", + "NVIDIA_API_KEY", + "NGC_API_KEY", + "GRAPHIFY_NVIDIA_NIM_MODEL", + "NVIDIA_NIM_MODEL", + "NIM_MODEL", + "NVIDIA_NIM_BASE_URL", + "NIM_BASE_URL", + "GRAPHIFY_DISABLE_NIM_FALLBACK", + "GRAPHIFY_MINIMAX_API_KEY", + "GRAPHIFY_MINIMAX_MODEL", + "MINIMAX_MODEL", "GEMINI_API_KEY", "GOOGLE_API_KEY", "MOONSHOT_API_KEY", @@ -18,8 +33,107 @@ def _clear_backend_env(monkeypatch): "DEEPSEEK_API_KEY", "AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", + "GRAPHIFY_DISABLE_OLLAMA_PRIMARY", + "GRAPHIFY_DISABLE_MINIMAX_FALLBACK", + "GRAPHIFY_OLLAMA_MODEL", + "OLLAMA_MODEL", + "OLLAMA_BASE_URL", + "OLLAMA_API_KEY", + "GRAPHIFY_OLLAMA_NUM_CTX", + "GRAPHIFY_OLLAMA_KEEP_ALIVE", + "GRAPHIFY_OLLAMA_FALLBACK_MODELS", + "GRAPHIFY_OLLAMA_PARALLEL", + "GRAPHIFY_OLLAMA_DAYTIME_POLICY", + "GRAPHIFY_OLLAMA_BALANCE", + "GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION", + "GRAPHIFY_OLLAMA_SLOW_CHUNK_SECONDS", + "GRAPHIFY_OLLAMA_DAYTIME_FILE_LIMIT", + "GRAPHIFY_OLLAMA_NUM_GPU", + "GRAPHIFY_OLLAMA_MAIN_GPU", + "GRAPHIFY_OLLAMA_NUM_THREAD", + "GRAPHIFY_OLLAMA_TOKEN_BUDGET", ): monkeypatch.delenv(env_key, raising=False) + monkeypatch.setenv("GRAPHIFY_DISABLE_OLLAMA_PRIMARY", "1") + +def test_ollama_is_default_primary_even_when_minimax_key_exists(monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.delenv("GRAPHIFY_DISABLE_OLLAMA_PRIMARY", raising=False) + monkeypatch.setenv("MINIMAX_API_KEY", "minimax-key") + + assert llm.detect_backend() == "ollama" + + +def test_oversized_ollama_model_does_not_block_safe_local_chain(monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.delenv("GRAPHIFY_DISABLE_OLLAMA_PRIMARY", raising=False) + monkeypatch.setenv("GRAPHIFY_OLLAMA_MODEL", "qwen3-coder:30b") + + assert llm.detect_backend() == "ollama" + assert llm._ollama_model_chain() == ["qwen2.5-coder:3b", "gemma3:4b"] + + +def test_oversized_ollama_model_still_prefers_safe_local_before_minimax(monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.delenv("GRAPHIFY_DISABLE_OLLAMA_PRIMARY", raising=False) + monkeypatch.setenv("GRAPHIFY_OLLAMA_MODEL", "qwen3-coder:30b") + monkeypatch.setenv("MINIMAX_API_KEY", "minimax-key") + + assert llm.detect_backend() == "ollama" + + +def test_minimax_accepts_minimax_api_key(monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.setenv("MINIMAX_API_KEY", "minimax-key") + + assert llm.detect_backend() == "minimax" + assert llm._get_backend_api_key("minimax") == "minimax-key" + + +def test_minimax_accepts_graphify_minimax_api_key(monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.setenv("GRAPHIFY_MINIMAX_API_KEY", "graphify-minimax-key") + + assert llm.detect_backend() == "minimax" + assert llm._get_backend_api_key("minimax") == "graphify-minimax-key" + + +def test_minimax_key_can_come_from_global_credentials(tmp_path, monkeypatch): + _clear_backend_env(monkeypatch) + creds = tmp_path / "credentials.json" + creds.write_text('{"api_keys":{"MINIMAX_API_KEY":"file-key"}}', encoding="utf-8") + monkeypatch.setenv("GRAPHIFY_CREDENTIALS_PATH", str(creds)) + + assert llm.detect_backend() == "minimax" + assert llm._get_backend_api_key("minimax") == "file-key" + + +def test_backend_detection_prefers_minimax(monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.setenv("OPENAI_API_KEY", "openai-key") + monkeypatch.setenv("ANTHROPIC_API_KEY", "anthropic-key") + monkeypatch.setenv("MOONSHOT_API_KEY", "moonshot-key") + monkeypatch.setenv("GEMINI_API_KEY", "gemini-key") + monkeypatch.setenv("MINIMAX_API_KEY", "minimax-key") + + assert llm.detect_backend() == "minimax" + +def test_nim_is_explicit_not_auto_detected(monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.setenv("OPENAI_API_KEY", "openai-key") + monkeypatch.setenv("NVIDIA_NIM_API_KEY", "nim-key") + + assert llm.detect_backend() == "openai" + assert llm._get_backend_api_key("nim") == "nim-key" + + +def test_minimax_still_preferred_over_nim(monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.setenv("NVIDIA_NIM_API_KEY", "nim-key") + monkeypatch.setenv("MINIMAX_API_KEY", "minimax-key") + + assert llm.detect_backend() == "minimax" + def test_gemini_accepts_gemini_api_key(monkeypatch): @@ -38,7 +152,7 @@ def test_gemini_accepts_google_api_key(monkeypatch): assert llm._get_backend_api_key("gemini") == "google-key" -def test_backend_detection_prefers_gemini(monkeypatch): +def test_backend_detection_prefers_gemini_when_minimax_unset(monkeypatch): _clear_backend_env(monkeypatch) monkeypatch.setenv("OPENAI_API_KEY", "openai-key") monkeypatch.setenv("ANTHROPIC_API_KEY", "anthropic-key") @@ -56,6 +170,87 @@ def test_openai_backend_detected(monkeypatch): assert llm._get_backend_api_key("openai") == "openai-key" +def test_extract_files_direct_routes_minimax_through_openai_compat(tmp_path, monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.setenv("MINIMAX_API_KEY", "minimax-key") + source = tmp_path / "note.md" + source.write_text("# Architecture\n\nThe runner emits a snapshot.\n") + result = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 1, "output_tokens": 1} + + with patch("graphify.llm._call_openai_compat", return_value=result) as call: + assert llm.extract_files_direct([source], backend="minimax", root=tmp_path) is result + + assert call.call_args.args[:3] == ( + "https://api.minimax.io/v1", + "minimax-key", + "MiniMax-M3", + ) + assert call.call_args.kwargs["temperature"] == 0 + assert call.call_args.kwargs["extra_body"] == {"thinking": {"type": "disabled"}} + assert call.call_args.kwargs["max_completion_tokens"] == 16384 + +def test_extract_files_direct_routes_nim_through_openai_compat(tmp_path, monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.setenv("NVIDIA_NIM_API_KEY", "nim-key") + source = tmp_path / "note.md" + source.write_text("# Architecture\n\nThe runner emits a snapshot.\n") + result = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 1, "output_tokens": 1} + + with patch("graphify.llm._call_openai_compat", return_value=result) as call: + assert llm.extract_files_direct([source], backend="nim", root=tmp_path) is result + + assert call.call_args.args[:3] == ( + "https://integrate.api.nvidia.com/v1", + "nim-key", + "meta/llama-3.1-8b-instruct", + ) + assert call.call_args.kwargs["temperature"] == 0 + assert call.call_args.kwargs["completion_token_param"] == "max_tokens" + assert call.call_args.kwargs["max_completion_tokens"] == 8192 + + +def test_ollama_falls_back_to_gemma_before_cloud(tmp_path, monkeypatch): + _clear_backend_env(monkeypatch) + source = tmp_path / "note.md" + source.write_text("# Architecture\n") + result = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 1, "output_tokens": 1} + + with patch("graphify.llm._call_ollama_native", side_effect=[RuntimeError("qwen down"), result]) as ollama_call: + assert llm.extract_files_direct([source], backend="ollama", root=tmp_path) is result + + models = [call.args[1] for call in ollama_call.call_args_list] + assert models == ["qwen2.5-coder:3b", "gemma3:4b"] + assert result["backend"] == "ollama" + + +def test_auto_ollama_falls_back_through_gemma_to_minimax_on_api_failure(tmp_path, monkeypatch): + _clear_backend_env(monkeypatch) + monkeypatch.delenv("GRAPHIFY_DISABLE_OLLAMA_PRIMARY", raising=False) + monkeypatch.setenv("MINIMAX_API_KEY", "minimax-key") + source = tmp_path / "note.md" + source.write_text("# Architecture\n") + result = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 1, "output_tokens": 1} + + with patch( + "graphify.llm._call_ollama_native", + side_effect=[RuntimeError("qwen down"), RuntimeError("gemma down")], + ) as ollama_call: + with patch("graphify.llm._call_openai_compat", return_value=result) as minimax_call: + assert llm.extract_files_direct([source], root=tmp_path) is result + + assert [call.args[1] for call in ollama_call.call_args_list] == [ + "qwen2.5-coder:3b", + "gemma3:4b", + ] + assert minimax_call.call_args.args[:3] == ( + "https://api.minimax.io/v1", + "minimax-key", + "MiniMax-M3", + ) + assert result["backend"] == "minimax" + + + def test_extract_files_direct_routes_gemini_through_openai_compat(tmp_path, monkeypatch): _clear_backend_env(monkeypatch) monkeypatch.setenv("GOOGLE_API_KEY", "google-key") @@ -282,7 +477,7 @@ def test_call_openai_compat_relabels_empty_content_as_length(monkeypatch): _install_fake_openai(monkeypatch, fake_resp) result = llm._call_openai_compat( - "http://localhost:11434/v1", "ollama", "qwen2.5-coder:7b", + "http://localhost:11434/v1", "ollama", "qwen2.5-coder:3b", "user msg", temperature=0, max_completion_tokens=8192, backend="ollama", ) assert result["finish_reason"] == "length", ( @@ -296,7 +491,7 @@ def test_call_openai_compat_relabels_none_content_as_length(monkeypatch): _install_fake_openai(monkeypatch, fake_resp) result = llm._call_openai_compat( - "http://localhost:11434/v1", "ollama", "qwen2.5-coder:7b", + "http://localhost:11434/v1", "ollama", "qwen2.5-coder:3b", "u", temperature=0, max_completion_tokens=8192, backend="ollama", ) assert result["finish_reason"] == "length" @@ -310,7 +505,7 @@ def test_call_openai_compat_relabels_unparseable_json_as_length(monkeypatch): _install_fake_openai(monkeypatch, fake_resp) result = llm._call_openai_compat( - "http://localhost:11434/v1", "ollama", "qwen2.5-coder:7b", + "http://localhost:11434/v1", "ollama", "qwen2.5-coder:3b", "u", temperature=0, max_completion_tokens=8192, backend="ollama", ) assert result["finish_reason"] == "length" @@ -371,7 +566,7 @@ def test_ollama_extra_body_sets_num_ctx_and_keep_alive(monkeypatch): monkeypatch.delenv("GRAPHIFY_OLLAMA_KEEP_ALIVE", raising=False) llm._call_openai_compat( - "http://localhost:11434/v1", "ollama", "qwen2.5-coder:7b", + "http://localhost:11434/v1", "ollama", "qwen2.5-coder:3b", "user msg", temperature=0, max_completion_tokens=8192, backend="ollama", ) @@ -380,7 +575,31 @@ def test_ollama_extra_body_sets_num_ctx_and_keep_alive(monkeypatch): # num_ctx is now dynamic: derived from message size, not hardcoded 131072 assert "num_ctx" in eb.get("options", {}), "num_ctx must be present" assert eb["options"]["num_ctx"] >= 8192, "num_ctx must be at least the floor value" - assert eb.get("keep_alive") == "30m", "default keep_alive must be 30m" + assert eb.get("keep_alive") == "30s", "default keep_alive must release VRAM quickly" + assert captured["response_format"] == {"type": "json_object"} + + +def test_ollama_json_mode_can_be_disabled_for_legacy_servers(monkeypatch): + captured = _install_capturing_openai(monkeypatch) + monkeypatch.setenv("GRAPHIFY_OLLAMA_JSON_MODE", "0") + + llm._call_openai_compat( + "http://localhost:11434/v1", "ollama", "qwen2.5-coder:3b", + "user msg", temperature=0, max_completion_tokens=8192, backend="ollama", + ) + + assert "response_format" not in captured + + +def test_non_ollama_openai_compat_does_not_force_json_mode(monkeypatch): + captured = _install_capturing_openai(monkeypatch) + + llm._call_openai_compat( + "https://api.openai.com/v1", "key", "gpt-4.1-mini", + "user msg", temperature=0, max_completion_tokens=8192, backend="openai", + ) + + assert "response_format" not in captured def test_ollama_num_ctx_scales_with_small_token_budget(monkeypatch): @@ -395,7 +614,7 @@ def test_ollama_num_ctx_scales_with_small_token_budget(monkeypatch): small_chunk_msg = "x" * 32_000 llm._call_openai_compat( - "http://localhost:11434/v1", "ollama", "qwen2.5-coder:7b", + "http://localhost:11434/v1", "ollama", "qwen2.5-coder:3b", small_chunk_msg, temperature=0, max_completion_tokens=16384, backend="ollama", ) @@ -415,13 +634,55 @@ def test_ollama_num_ctx_env_override(monkeypatch): monkeypatch.delenv("GRAPHIFY_OLLAMA_KEEP_ALIVE", raising=False) llm._call_openai_compat( - "http://localhost:11434/v1", "ollama", "qwen2.5-coder:7b", + "http://localhost:11434/v1", "ollama", "qwen2.5-coder:3b", "u", temperature=0, max_completion_tokens=8192, backend="ollama", ) assert captured["extra_body"]["options"]["num_ctx"] == 65536 +def test_ollama_native_uses_api_chat_and_num_ctx(monkeypatch): + captured = {} + + class _Resp: + def __enter__(self): + return self + + def __exit__(self, *_): + return False + + def read(self): + return ( + b'{"message":{"content":"{\\"nodes\\":[{\\"id\\":\\"x\\"}],' + b'\\"edges\\":[],\\"hyperedges\\":[]}"},"prompt_eval_count":10,' + b'"eval_count":100,"done_reason":"stop"}' + ) + + def fake_urlopen(request, timeout): + captured["url"] = request.full_url + captured["timeout"] = timeout + captured["payload"] = json.loads(request.data.decode("utf-8")) + return _Resp() + + monkeypatch.setenv("GRAPHIFY_OLLAMA_NUM_CTX", "65536") + monkeypatch.setenv("GRAPHIFY_OLLAMA_KEEP_ALIVE", "0") + monkeypatch.setattr(llm.urllib.request, "urlopen", fake_urlopen) + + result = llm._call_ollama_native( + "http://localhost:11434/v1", + "gemma3:4b", + "u", + max_completion_tokens=8192, + ) + + assert captured["url"] == "http://localhost:11434/api/chat" + assert captured["payload"]["options"]["num_ctx"] == 65536 + assert captured["payload"]["options"]["num_predict"] == 8192 + assert captured["payload"]["keep_alive"] == "0" + assert captured["payload"]["format"] == "json" + assert result["nodes"] == [{"id": "x"}] + + def test_non_ollama_backend_gets_no_num_ctx_extra_body(monkeypatch): captured = _install_capturing_openai(monkeypatch) @@ -476,7 +737,7 @@ def test_call_openai_compat_explicit_extra_body_skips_ollama_auto_derive(monkeyp monkeypatch.delenv("GRAPHIFY_OLLAMA_KEEP_ALIVE", raising=False) llm._call_openai_compat( - "http://localhost:11434/v1", "ollama", "qwen2.5-coder:7b", + "http://localhost:11434/v1", "ollama", "qwen2.5-coder:3b", "u", temperature=0, max_completion_tokens=8192, backend="ollama", extra_body={"options": {"num_ctx": 4096}}, ) @@ -486,6 +747,55 @@ def test_call_openai_compat_explicit_extra_body_skips_ollama_auto_derive(monkeyp ) +def test_extract_corpus_parallel_spills_only_some_ollama_chunks_to_minimax(tmp_path, monkeypatch): + files = [tmp_path / f"f{i}.md" for i in range(4)] + for f in files: + f.write_text("hello") + monkeypatch.setenv("MINIMAX_API_KEY", "minimax-key") + monkeypatch.setenv("GRAPHIFY_OLLAMA_DAYTIME_FILE_LIMIT", "3") + monkeypatch.delenv("GRAPHIFY_OLLAMA_BALANCE", raising=False) + + with patch("graphify.llm._ollama_system_pressure", return_value="high"): + with patch("graphify.llm.extract_files_direct", side_effect=lambda *args, **kwargs: _ok()) as call: + result = llm.extract_corpus_parallel( + files, + backend="ollama", + api_key="ollama", + model="qwen2.5-coder:3b", + root=tmp_path, + token_budget=None, + chunk_size=1, + allow_minimax_fallback=True, + ) + + backends = [c.kwargs["backend"] for c in call.call_args_list] + assert backends[0] == "minimax" + assert backends.count("minimax") == 1 + assert result["minimax_chunks"] == 1 + + +def test_extract_corpus_parallel_can_defer_daytime_semantics(tmp_path, monkeypatch): + files = [tmp_path / f"f{i}.md" for i in range(3)] + for f in files: + f.write_text("hello") + monkeypatch.setenv("GRAPHIFY_OLLAMA_BALANCE", "defer") + monkeypatch.setenv("GRAPHIFY_OLLAMA_DAYTIME_FILE_LIMIT", "3") + + with patch("graphify.llm._in_ollama_low_load_window", return_value=False): + with patch("graphify.llm.extract_files_direct") as call: + result = llm.extract_corpus_parallel( + files, + backend="ollama", + api_key="ollama", + model="qwen2.5-coder:3b", + root=tmp_path, + token_budget=None, + chunk_size=1, + ) + + call.assert_not_called() + assert result["deferred_semantic"] is True + def test_extract_corpus_parallel_ollama_runs_serially(tmp_path, monkeypatch): # With 3 chunks and backend=ollama, ThreadPoolExecutor must NOT be used # (workers=1 takes the sequential path). We verify by ensuring all chunks @@ -505,7 +815,7 @@ def fake_extract(chunk, *_, **__): with patch("graphify.llm.extract_files_direct", side_effect=fake_extract): with patch("graphify.llm.ThreadPoolExecutor") as mock_pool: result = llm.extract_corpus_parallel( - files, backend="ollama", api_key="ollama", model="qwen2.5-coder:7b", + files, backend="ollama", api_key="ollama", model="qwen2.5-coder:3b", root=tmp_path, token_budget=None, chunk_size=2, max_concurrency=4, ) @@ -513,6 +823,30 @@ def fake_extract(chunk, *_, **__): assert len(result["nodes"]) == 6 +def test_extract_corpus_parallel_ollama_uses_local_token_budget(tmp_path, monkeypatch): + files = [tmp_path / "a.md"] + files[0].write_text("hello") + captured = {} + + def fake_pack(paths, token_budget): + captured["token_budget"] = token_budget + return [paths] + + monkeypatch.delenv("GRAPHIFY_OLLAMA_TOKEN_BUDGET", raising=False) + with patch("graphify.llm._pack_chunks_by_tokens", side_effect=fake_pack): + with patch("graphify.llm.extract_files_direct", return_value=_ok()): + llm.extract_corpus_parallel( + files, + backend="ollama", + api_key="ollama", + model="qwen2.5-coder:3b", + root=tmp_path, + max_concurrency=1, + ) + + assert captured["token_budget"] == 20_000 + + def test_extract_corpus_parallel_ollama_parallel_env_restores_concurrency(tmp_path, monkeypatch): files = [tmp_path / f"f{i}.md" for i in range(4)] for f in files: @@ -529,7 +863,7 @@ def test_extract_corpus_parallel_ollama_parallel_env_restores_concurrency(tmp_pa )() try: llm.extract_corpus_parallel( - files, backend="ollama", api_key="ollama", model="m", + files, backend="ollama", api_key="ollama", model="qwen2.5-coder:3b", root=tmp_path, token_budget=None, chunk_size=2, max_concurrency=4, ) except Exception: @@ -564,7 +898,7 @@ def fake_extract(chunk, *_, **__): with patch("graphify.llm.extract_files_direct", side_effect=fake_extract): result = llm._extract_with_adaptive_retry( - files, backend="ollama", api_key="ollama", model="qwen2.5-coder:7b", + files, backend="ollama", api_key="ollama", model="qwen2.5-coder:3b", root=tmp_path, max_depth=3, ) @@ -617,10 +951,10 @@ def test_call_azure_uses_correct_client_params_and_max_completion_tokens(monkeyp captured = _install_fake_azure_openai(monkeypatch, fake_resp) result = llm._call_azure( - api_key="test-key", endpoint="https://my-resource.openai.azure.com/", model="gpt-4o", user_message="test", + **{"api_key": "dummy"}, ) assert captured["init_kwargs"].get("azure_endpoint") == "https://my-resource.openai.azure.com/" diff --git a/tests/test_multigraph_diagnostics.py b/tests/test_multigraph_diagnostics.py index 8c39b8e23..cb125a39d 100644 --- a/tests/test_multigraph_diagnostics.py +++ b/tests/test_multigraph_diagnostics.py @@ -147,7 +147,7 @@ def test_diagnose_extraction_handles_malformed_shapes_without_crashing() -> None assert summary["missing_endpoint_edges"] == 1 assert summary["dangling_endpoint_edges"] == 2 assert summary["valid_candidate_edges"] == 1 - assert summary["post_build_error"].startswith("TypeError:") + assert summary["post_build_error"] == "" def test_diagnose_extraction_handles_non_list_nodes_and_edges() -> None: @@ -228,7 +228,7 @@ def test_format_diagnostic_report_includes_build_and_suppression_errors( report = format_diagnostic_report(summary) - assert "post_build_error: TypeError:" in report + assert "post_build_error:" not in report assert "producer_suppression_error: file not found" in report diff --git a/tests/test_ollama.py b/tests/test_ollama.py index c90d610f3..bf5e673a7 100644 --- a/tests/test_ollama.py +++ b/tests/test_ollama.py @@ -54,41 +54,47 @@ def test_ollama_in_backends(): assert BACKENDS["ollama"]["pricing"]["output"] == 0.0 assert "max_tokens" in BACKENDS["ollama"] +def _clear_non_ollama_keys(monkeypatch): + for key in ( + "MINIMAX_API_KEY", "GRAPHIFY_MINIMAX_API_KEY", + "GEMINI_API_KEY", "GOOGLE_API_KEY", "MOONSHOT_API_KEY", + "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", + "AWS_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION", + "OLLAMA_BASE_URL", "OLLAMA_MODEL", "GRAPHIFY_OLLAMA_MODEL", + "GRAPHIFY_DISABLE_OLLAMA_PRIMARY", + ): + monkeypatch.delenv(key, raising=False) + + def test_detect_backend_ollama(monkeypatch): - monkeypatch.delenv("MOONSHOT_API_KEY", raising=False) - monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + _clear_non_ollama_keys(monkeypatch) monkeypatch.setenv("OLLAMA_BASE_URL", "http://localhost:11434/v1") assert detect_backend() == "ollama" -def test_detect_backend_kimi_beats_ollama(monkeypatch): +def test_detect_backend_ollama_beats_kimi(monkeypatch): + _clear_non_ollama_keys(monkeypatch) monkeypatch.setenv("MOONSHOT_API_KEY", "test-key") monkeypatch.setenv("OLLAMA_BASE_URL", "http://localhost:11434/v1") - monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) - assert detect_backend() == "kimi" + assert detect_backend() == "ollama" -def test_detect_backend_claude_beats_ollama(monkeypatch): - # ANTHROPIC_API_KEY (paid, intentional) should win over OLLAMA_BASE_URL - # (env-driven, easy to set accidentally) -- security fix F-002/F-029. - monkeypatch.delenv("MOONSHOT_API_KEY", raising=False) - monkeypatch.delenv("GEMINI_API_KEY", raising=False) - monkeypatch.delenv("GOOGLE_API_KEY", raising=False) +def test_detect_backend_ollama_beats_claude(monkeypatch): + _clear_non_ollama_keys(monkeypatch) monkeypatch.setenv("OLLAMA_BASE_URL", "http://localhost:11434/v1") monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test") - assert detect_backend() == "claude" + assert detect_backend() == "ollama" -def test_detect_backend_none_without_envvars(monkeypatch): - monkeypatch.delenv("MOONSHOT_API_KEY", raising=False) - monkeypatch.delenv("OLLAMA_BASE_URL", raising=False) - monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) +def test_detect_backend_none_when_ollama_primary_disabled(monkeypatch): + _clear_non_ollama_keys(monkeypatch) + monkeypatch.setenv("GRAPHIFY_DISABLE_OLLAMA_PRIMARY", "1") assert detect_backend() is None -def test_ollama_api_key_sentinel(monkeypatch): - """extract_files_direct with backend=ollama and no OLLAMA_API_KEY should use sentinel 'ollama' not raise.""" +def test_ollama_native_backend_does_not_require_api_key(monkeypatch): + """extract_files_direct with backend=ollama and no OLLAMA_API_KEY should not raise.""" monkeypatch.delenv("OLLAMA_API_KEY", raising=False) from unittest.mock import patch from pathlib import Path @@ -102,17 +108,13 @@ def test_ollama_api_key_sentinel(monkeypatch): "output_tokens": 10, "finish_reason": "stop", } - with patch("graphify.llm._call_openai_compat", return_value=fake_result) as mock_call: + with patch("graphify.llm._call_ollama_native", return_value=fake_result) as mock_call: from graphify.llm import extract_files_direct with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as f: f.write("x = 1\n") tmp = Path(f.name) try: extract_files_direct([tmp], backend="ollama", root=tmp.parent) - # Should have called _call_openai_compat with api_key="ollama" assert mock_call.called - call_kwargs = mock_call.call_args - api_key_used = call_kwargs.args[1] if call_kwargs.args else call_kwargs.kwargs.get("api_key", "") - assert api_key_used == "ollama" finally: tmp.unlink(missing_ok=True) diff --git a/tests/test_provider_registry.py b/tests/test_provider_registry.py index 0366c13ff..e271172c2 100644 --- a/tests/test_provider_registry.py +++ b/tests/test_provider_registry.py @@ -153,12 +153,14 @@ def test_detect_backend_custom_provider_after_builtins(monkeypatch): } }) monkeypatch.setenv("MY_CUSTOM_KEY", "test-key") - for key in ("GEMINI_API_KEY", "GOOGLE_API_KEY", "MOONSHOT_API_KEY", "ANTHROPIC_API_KEY", + for key in ("MINIMAX_API_KEY", "GRAPHIFY_MINIMAX_API_KEY", + "GEMINI_API_KEY", "GOOGLE_API_KEY", "MOONSHOT_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "OLLAMA_BASE_URL"): monkeypatch.delenv(key, raising=False) monkeypatch.delenv("AWS_PROFILE", raising=False) monkeypatch.delenv("AWS_REGION", raising=False) monkeypatch.delenv("AWS_DEFAULT_REGION", raising=False) + monkeypatch.setenv("GRAPHIFY_DISABLE_OLLAMA_PRIMARY", "1") result = llm.detect_backend() assert result == "myprovider" diff --git a/tests/test_watch.py b/tests/test_watch.py index 8dcdf4999..ecb47aeb7 100644 --- a/tests/test_watch.py +++ b/tests/test_watch.py @@ -7,7 +7,7 @@ from pathlib import Path import pytest -from graphify.watch import _notify_only, _WATCHED_EXTENSIONS, _rebuild_lock, _check_shrink +from graphify.watch import _notify_only, _WATCHED_EXTENSIONS, _rebuild_lock, _check_shrink, _record_daily_update_hint # --- _notify_only --- @@ -86,6 +86,31 @@ def test_check_update_does_not_clear_flag(tmp_path): assert flag.exists() +def test_daily_update_hint_for_active_github_repo(tmp_path, monkeypatch): + monkeypatch.setenv("GRAPHIFY_DAILY_UPDATE_ROOT", str(tmp_path)) + monkeypatch.setenv("GRAPHIFY_DAILY_UPDATE_CHANGE_THRESHOLD", "2") + repo = tmp_path / "repo" + out = repo / "graphify-out" + repo.mkdir() + + _record_daily_update_hint(out, repo, [repo / "a.py", repo / "b.py"]) + + hint = json.loads((out / "nightly-update-hint.json").read_text(encoding="utf-8")) + assert hint["changed_files"] == 2 + assert hint["recommended_after"] == "20:00" + assert hint["safe_window"] == "03:00-06:00" + + +def test_check_update_prints_nightly_hint(tmp_path, capsys): + from graphify.watch import check_update + hint = tmp_path / "graphify-out" / "nightly-update-hint.json" + hint.parent.mkdir(parents=True, exist_ok=True) + hint.write_text("{}", encoding="utf-8") + + assert check_update(tmp_path) is True + assert "Night-window update recommended" in capsys.readouterr().out + + def test_watch_raises_without_watchdog(tmp_path, monkeypatch): import builtins real_import = builtins.__import__ @@ -255,6 +280,11 @@ def edges(d): "file_type": "concept", "source_file": "a.py", }) + data["nodes"].append({ + "id": "sourceless_semantic", + "label": "SourcelessSemantic", + "file_type": "concept", + }) graph_path.write_text(json.dumps(data), encoding="utf-8") # Remove foo() from a.py (keep bar); leave b.py untouched. @@ -272,6 +302,7 @@ def edges(d): assert "bar()" in after, "surviving symbol in the same file must be kept" assert "caller()" in after, "unchanged file's nodes must be kept" assert "AuthConcept" in after, "semantic node on a surviving file must not be evicted" + assert "SourcelessSemantic" not in after, "sourceless semantic noise must be evicted on full rebuild" def test_rebuild_code_preupgrade_marker_less_node_one_cycle_lag(tmp_path): diff --git a/tools/skillgen/expected/graphify__skill-amp.md b/tools/skillgen/expected/graphify__skill-amp.md index 56f7b8692..10d1457a2 100644 --- a/tools/skillgen/expected/graphify__skill-amp.md +++ b/tools/skillgen/expected/graphify__skill-amp.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skill-claw.md b/tools/skillgen/expected/graphify__skill-claw.md index db3905b10..862e58083 100644 --- a/tools/skillgen/expected/graphify__skill-claw.md +++ b/tools/skillgen/expected/graphify__skill-claw.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skill-codex.md b/tools/skillgen/expected/graphify__skill-codex.md index d63892df0..7c06dc28b 100644 --- a/tools/skillgen/expected/graphify__skill-codex.md +++ b/tools/skillgen/expected/graphify__skill-codex.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skill-copilot.md b/tools/skillgen/expected/graphify__skill-copilot.md index db3905b10..862e58083 100644 --- a/tools/skillgen/expected/graphify__skill-copilot.md +++ b/tools/skillgen/expected/graphify__skill-copilot.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skill-droid.md b/tools/skillgen/expected/graphify__skill-droid.md index 0238913d6..a51fa0cb8 100644 --- a/tools/skillgen/expected/graphify__skill-droid.md +++ b/tools/skillgen/expected/graphify__skill-droid.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skill-kilo.md b/tools/skillgen/expected/graphify__skill-kilo.md index 2cb0e1f6b..0d45096c0 100644 --- a/tools/skillgen/expected/graphify__skill-kilo.md +++ b/tools/skillgen/expected/graphify__skill-kilo.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skill-kiro.md b/tools/skillgen/expected/graphify__skill-kiro.md index db3905b10..862e58083 100644 --- a/tools/skillgen/expected/graphify__skill-kiro.md +++ b/tools/skillgen/expected/graphify__skill-kiro.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skill-opencode.md b/tools/skillgen/expected/graphify__skill-opencode.md index 31ef311b7..261758261 100644 --- a/tools/skillgen/expected/graphify__skill-opencode.md +++ b/tools/skillgen/expected/graphify__skill-opencode.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skill-pi.md b/tools/skillgen/expected/graphify__skill-pi.md index db3905b10..862e58083 100644 --- a/tools/skillgen/expected/graphify__skill-pi.md +++ b/tools/skillgen/expected/graphify__skill-pi.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skill-trae.md b/tools/skillgen/expected/graphify__skill-trae.md index f27e2fc20..5cfde567f 100644 --- a/tools/skillgen/expected/graphify__skill-trae.md +++ b/tools/skillgen/expected/graphify__skill-trae.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skill-vscode.md b/tools/skillgen/expected/graphify__skill-vscode.md index 764fe51d6..531bcfbb5 100644 --- a/tools/skillgen/expected/graphify__skill-vscode.md +++ b/tools/skillgen/expected/graphify__skill-vscode.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skill-windows.md b/tools/skillgen/expected/graphify__skill-windows.md index f943feaab..08241b311 100644 --- a/tools/skillgen/expected/graphify__skill-windows.md +++ b/tools/skillgen/expected/graphify__skill-windows.md @@ -173,12 +173,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skill.md b/tools/skillgen/expected/graphify__skill.md index 1ad666729..5bb8e4b5c 100644 --- a/tools/skillgen/expected/graphify__skill.md +++ b/tools/skillgen/expected/graphify__skill.md @@ -151,12 +151,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/expected/graphify__skills__amp__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__amp__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__amp__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__amp__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__amp__references__update.md b/tools/skillgen/expected/graphify__skills__amp__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__amp__references__update.md +++ b/tools/skillgen/expected/graphify__skills__amp__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/expected/graphify__skills__claude__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__claude__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__claude__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__claude__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__claude__references__update.md b/tools/skillgen/expected/graphify__skills__claude__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__claude__references__update.md +++ b/tools/skillgen/expected/graphify__skills__claude__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/expected/graphify__skills__claw__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__claw__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__claw__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__claw__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__claw__references__update.md b/tools/skillgen/expected/graphify__skills__claw__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__claw__references__update.md +++ b/tools/skillgen/expected/graphify__skills__claw__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/expected/graphify__skills__codex__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__codex__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__codex__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__codex__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__codex__references__update.md b/tools/skillgen/expected/graphify__skills__codex__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__codex__references__update.md +++ b/tools/skillgen/expected/graphify__skills__codex__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/expected/graphify__skills__copilot__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__copilot__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__copilot__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__copilot__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__copilot__references__update.md b/tools/skillgen/expected/graphify__skills__copilot__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__copilot__references__update.md +++ b/tools/skillgen/expected/graphify__skills__copilot__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/expected/graphify__skills__droid__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__droid__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__droid__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__droid__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__droid__references__update.md b/tools/skillgen/expected/graphify__skills__droid__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__droid__references__update.md +++ b/tools/skillgen/expected/graphify__skills__droid__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/expected/graphify__skills__kilo__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__kilo__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__kilo__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__kilo__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__kilo__references__update.md b/tools/skillgen/expected/graphify__skills__kilo__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__kilo__references__update.md +++ b/tools/skillgen/expected/graphify__skills__kilo__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/expected/graphify__skills__kiro__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__kiro__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__kiro__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__kiro__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__kiro__references__update.md b/tools/skillgen/expected/graphify__skills__kiro__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__kiro__references__update.md +++ b/tools/skillgen/expected/graphify__skills__kiro__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/expected/graphify__skills__opencode__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__opencode__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__opencode__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__opencode__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__opencode__references__update.md b/tools/skillgen/expected/graphify__skills__opencode__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__opencode__references__update.md +++ b/tools/skillgen/expected/graphify__skills__opencode__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/expected/graphify__skills__pi__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__pi__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__pi__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__pi__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__pi__references__update.md b/tools/skillgen/expected/graphify__skills__pi__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__pi__references__update.md +++ b/tools/skillgen/expected/graphify__skills__pi__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/expected/graphify__skills__trae__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__trae__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__trae__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__trae__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__trae__references__update.md b/tools/skillgen/expected/graphify__skills__trae__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__trae__references__update.md +++ b/tools/skillgen/expected/graphify__skills__trae__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/expected/graphify__skills__vscode__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__vscode__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__vscode__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__vscode__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__vscode__references__update.md b/tools/skillgen/expected/graphify__skills__vscode__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__vscode__references__update.md +++ b/tools/skillgen/expected/graphify__skills__vscode__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/expected/graphify__skills__windows__references__github-and-merge.md b/tools/skillgen/expected/graphify__skills__windows__references__github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/expected/graphify__skills__windows__references__github-and-merge.md +++ b/tools/skillgen/expected/graphify__skills__windows__references__github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/expected/graphify__skills__windows__references__update.md b/tools/skillgen/expected/graphify__skills__windows__references__update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/expected/graphify__skills__windows__references__update.md +++ b/tools/skillgen/expected/graphify__skills__windows__references__update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/tools/skillgen/fragments/core/core.md b/tools/skillgen/fragments/core/core.md index 2d0c48164..eab17660f 100644 --- a/tools/skillgen/fragments/core/core.md +++ b/tools/skillgen/fragments/core/core.md @@ -110,12 +110,16 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). -**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: -> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). +**Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. -Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. +MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. -> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. +For workflow-needed indexing, run the AST path immediately (`graphify update .`) and let semantic extraction run opportunistically. Large semantic rebuilds that can wait should use `GRAPHIFY_OLLAMA_BALANCE=defer` during the day; graphify records the queued night rebuild hint in `graphify-out/semantic-rebuild-queue.jsonl`. Prefer 20:00-06:00 for heavy local rebuilds, with 03:00-06:00 as the safest window. + +If neither local Ollama nor MiniMax can run, print this one-liner to the user, then continue with the host-model subagent dispatch in Part B: +> Tip: start Ollama with `qwen2.5-coder:3b` for local <=8B-class semantic extraction, or set `MINIMAX_API_KEY` in `~/.graphify/credentials.json` for MiniMax fallback (`pip install 'graphifyy[ollama,minimax]'`). + +> **No other API keys are read by the automatic skill path.** If Ollama/MiniMax are unavailable, fall straight through to host subagent dispatch (Part B below) — the host session itself is the LLM. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. **Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** diff --git a/tools/skillgen/fragments/references/shared/github-and-merge.md b/tools/skillgen/fragments/references/shared/github-and-merge.md index a41ea06e1..48b4297b8 100644 --- a/tools/skillgen/fragments/references/shared/github-and-merge.md +++ b/tools/skillgen/fragments/references/shared/github-and-merge.md @@ -33,7 +33,7 @@ The skill pipeline writes all intermediate and final outputs to `graphify-out/` graphify extract ./core/ # → ./core/graphify-out/graph.json graphify extract ./service/ # → ./service/graphify-out/graph.json graphify extract ./platform/ # → ./platform/graphify-out/graph.json -# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set +# Add --backend minimax|nim|gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set # Then merge at the project root: graphify merge-graphs \ diff --git a/tools/skillgen/fragments/references/shared/update.md b/tools/skillgen/fragments/references/shared/update.md index d35b665e7..67afbc779 100644 --- a/tools/skillgen/fragments/references/shared/update.md +++ b/tools/skillgen/fragments/references/shared/update.md @@ -6,6 +6,8 @@ Load this only when the user passed `--update` or `--cluster-only`. A first-time Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. +For very large repos under `/media/naray/backup_np_2/github/`, do not run semantic `--update` repeatedly during the day. Let automatic AST indexing keep code workflows fresh, and reserve this full update path for an explicit user request or one daily night-window refresh after 20:00 (03:00-06:00 safest). If `graphify-out/nightly-update-hint.json` exists, treat it as the queue hint for that daily refresh. + ```bash $(cat graphify-out/.graphify_python) -c " import sys, json diff --git a/uv.lock b/uv.lock index 8010f68f2..2860a68d6 100644 --- a/uv.lock +++ b/uv.lock @@ -2,14 +2,9 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.13'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -34,7 +29,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.105.2" +version = "0.109.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -46,9 +41,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/46/47581b8c689c743ceabf6a0f9ff48472160900ce802d26c0fb50423997b3/anthropic-0.105.2.tar.gz", hash = "sha256:0e26b90841c2dced7cc6e98d21d5517d0be33f1876b8e779f478202e28bcaa07", size = 853789, upload-time = "2026-05-29T00:21:14.104Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/0b/ce24a4f275573f5e436ca954faca60c759d58ed152b8fa36a1e3b888e261/anthropic-0.109.1.tar.gz", hash = "sha256:83e06b3d9d40ff5898f588020e0cc4e42187de954549a3b5fbe6e2685a09c785", size = 927569, upload-time = "2026-06-09T23:55:24.884Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/75/be0c357e33a5a56c8f9db5b4212f886138d2bf59c0952d858f6b75d710ef/anthropic-0.105.2-py3-none-any.whl", hash = "sha256:e53ed5f6bf36fb1ecb9b25d8634cfd30e02fab9fb3374a0c2d5c585874757230", size = 837507, upload-time = "2026-05-29T00:21:15.528Z" }, + { url = "https://files.pythonhosted.org/packages/91/0f/a6110d713370bc92f074a622f8a5ebdec7e92360149b1048dca258a07b2f/anthropic-0.109.1-py3-none-any.whl", hash = "sha256:ce7d94a7657f2aa29338cca448945eac621b4f62c1794cf461cb32847223e9b8", size = 923851, upload-time = "2026-06-09T23:55:23.348Z" }, ] [[package]] @@ -74,6 +69,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/98/f6aa7fe0783e42be3093d8ef1b0ecdc22c34c0d69640dfb37f56925cb141/anytree-2.13.0-py3-none-any.whl", hash = "sha256:4cbcf10df36b1f1cba131b7e487ff3edafc9d6e932a3c70071b5b768bab901ff", size = 45077, upload-time = "2025-04-08T21:06:29.494Z" }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + [[package]] name = "attrs" version = "26.1.0" @@ -110,33 +114,39 @@ wheels = [ [[package]] name = "av" -version = "17.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/f0/8c8dca97ae0cf00e8e2a53bb5cb9aca5fd484f585ef3e9b412200aff3ebd/av-17.0.1.tar.gz", hash = "sha256:fbcbd4aa43bca6a8691816283112d1659a27f407bbeb66d1397023691339f5d4", size = 4411938, upload-time = "2026-04-18T17:12:34.29Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/0c/cbc39b090ec8d30ff795f1fd2cde1b686d1943051cb11a6ba699a10c95cd/av-17.0.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:985c21095bfb9c4bb7ba362fbef7bf0194bd72b1d7d3c46e30d1f47c5d38b4df", size = 23409596, upload-time = "2026-04-18T17:11:32.829Z" }, - { url = "https://files.pythonhosted.org/packages/01/cf/f92dc08c14c6f6fd89f98c25803f2024dbc6a43894e371925181a7d7a120/av-17.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:f585358fe0127990aea7887e940de4cdd745a2770605c31e54b2418fd0fdd8bd", size = 18831018, upload-time = "2026-04-18T17:11:35.098Z" }, - { url = "https://files.pythonhosted.org/packages/a3/38/1769c0315df060f9631727ac757e20d36f9413a9f7fa8b085ed1ccd69001/av-17.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:50f9dd53a8ebef77606dca3b21710f660f9a6478484e79b9abda7c787b4f2403", size = 35336690, upload-time = "2026-04-18T17:11:37.707Z" }, - { url = "https://files.pythonhosted.org/packages/e4/9c/6f2abe6179e9828f6e334201a6d3ca14e90e6eb4fb5ff0ccca68e7b0beb2/av-17.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8270634c409f8efc9a24216e5dd90313d873b26ea4b5f172b14de52cbd15121c", size = 37669836, upload-time = "2026-04-18T17:11:40.23Z" }, - { url = "https://files.pythonhosted.org/packages/a1/0b/f050ba5d3f294a2250f8b64eaa6059fc6df39573e5960f5833850aa50033/av-17.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3a3f33bbfed2bcc65be37941bfeb6cc20bbe9cb7afc4ef1ac8d330972df098f9", size = 36536999, upload-time = "2026-04-18T17:11:42.944Z" }, - { url = "https://files.pythonhosted.org/packages/cf/31/f9ed99d4c483bdb3695b7f4d5997cb2dc0b2d57ce1a6d28bce867b5ddaf9/av-17.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09b1f1601cc4a4d9e616d197b345c363ba6abfe567cb3d6b18e45516126692b6", size = 38800109, upload-time = "2026-04-18T17:11:45.834Z" }, - { url = "https://files.pythonhosted.org/packages/14/30/9b6c933458a585508b4585dba552b2bad57ef17908bcff109275b1eb9a39/av-17.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:f63b30067e6d88a3cce0d73d01ecfc0e6f091ad2bcf689db5dc305b0b4e8348c", size = 28985245, upload-time = "2026-04-18T17:11:48.698Z" }, - { url = "https://files.pythonhosted.org/packages/4c/82/e7007dcef7bd2d2c377e2e85977701384f42d19fc808c2ccb3a99eaf58f2/av-17.0.1-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:987f4f46ceae4da6c614dcbd2b8149be9dbf680c3bb7a6841c58af9cff4d9230", size = 23238802, upload-time = "2026-04-18T17:11:51.166Z" }, - { url = "https://files.pythonhosted.org/packages/6b/aa/858b09a08ea6f83f91be44b5a5adad13ae8d9ac8b80fda27e73c24bfb160/av-17.0.1-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:d97f54e55b18a74912f479c1978aadd1341d38d892dee95bb5c2f2dccfa72f32", size = 18709338, upload-time = "2026-04-18T17:11:53.286Z" }, - { url = "https://files.pythonhosted.org/packages/a8/8b/8de3fd21c4b0b74d44337421abeab0e71462337fb6a28fff888e0c356cbd/av-17.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e6eee84afa48d0e9321047cd3e4facd44b401493f6bdc753e2e1d1e7c9e6d13e", size = 34007351, upload-time = "2026-04-18T17:11:56.116Z" }, - { url = "https://files.pythonhosted.org/packages/02/28/167b291356c2cc315a2d62a95b0ceace72b5b0bf547de30b89313110f032/av-17.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c58c71bffd9383908c85695ac61d3184c668accb04a5bd1b262e0fb8d09f60a5", size = 36345295, upload-time = "2026-04-18T17:11:59.125Z" }, - { url = "https://files.pythonhosted.org/packages/04/fa/aae56f2ff2c204c408641e1120f5ca5ce9c3390cf5362245c6f1158704b5/av-17.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:42d6745d30a410ec9b22aef79a52a7ab5a001eb8f5adfd952946606a30983318", size = 35183754, upload-time = "2026-04-18T17:12:01.697Z" }, - { url = "https://files.pythonhosted.org/packages/ba/bd/776046f27093aef80155a204ca7d82a887ae4ee72ba4ef8411b46ea7898c/av-17.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3ed6bcd7021fe55832f95b8ef78dd01a4cb21faf3cd71f1e1bf4f20bf100b278", size = 37430809, upload-time = "2026-04-18T17:12:04.231Z" }, - { url = "https://files.pythonhosted.org/packages/d9/d5/3261bd2c6b7f6c0aa8379fc970d1ecf496330990b992ad28607785074268/av-17.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:9af524e8632a54032e361d6b88895bd3e7c6212ca560de60f5ccc525323c764c", size = 28889649, upload-time = "2026-04-18T17:12:07.04Z" }, - { url = "https://files.pythonhosted.org/packages/98/39/381104e427a0c7231d2ec0d25d538d58fc20fc0458846b95860d3ef8073b/av-17.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:50e58a473d65ea29b645e45c9fd8518a6783737135683ecc40571a91592bdfe4", size = 21918412, upload-time = "2026-04-18T17:12:09.312Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8c/bb1498f031abb6157b30b7fc2379359176953821b6ba59fbd89dbb56f61f/av-17.0.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:1d33871742d1e71562db3c8e752cacc5a62766d7efc3ae408bff1c3e26ebb46e", size = 23484157, upload-time = "2026-04-18T17:12:11.67Z" }, - { url = "https://files.pythonhosted.org/packages/1a/58/dedaef187b797243cd5762722e376c69c5ad95ab23db44127f09afc2cd66/av-17.0.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1229e879f4b6431bc00f69d7f8891fe9a683b0a6e0e009e6c98eb7e449f0383d", size = 18920872, upload-time = "2026-04-18T17:12:14.826Z" }, - { url = "https://files.pythonhosted.org/packages/9b/26/5c550231651d6285e6a5c4f6f4a0e67459bfe2b622a7c9352be8cca8c819/av-17.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4744837f4116964280bcc72285e3cdd51361e98a696205aadd924203440ef511", size = 37471077, upload-time = "2026-04-18T17:12:17.349Z" }, - { url = "https://files.pythonhosted.org/packages/59/e4/9807b89a9d775c6f015677996c48bce48aaff70b5d95885adf39e59832a2/av-17.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3d0a7d45d9599bf9df9f8249827113d4f36df1cd6b5356227b997f0552dbc98e", size = 39566981, upload-time = "2026-04-18T17:12:19.942Z" }, - { url = "https://files.pythonhosted.org/packages/5c/72/a22a657abc3de652f5b4f46cbbebdf7cba629752112791b81f05d340991d/av-17.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9acd0b6a6e02af2b37f63d97a03ee2c47936d58e82425c3cd075a95245937c59", size = 38397369, upload-time = "2026-04-18T17:12:22.909Z" }, - { url = "https://files.pythonhosted.org/packages/ae/b2/f4e83e41c1e3c186f34b7df506779d0cd7e40499e2e19519c7ece148cd20/av-17.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3d3a36204cb1f1e7691e6446afa8d6b7097b09946dae732c71c5d05ce09e506e", size = 40582445, upload-time = "2026-04-18T17:12:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/c8/59/8676188b72eed09d48ce6cfaf0f22b0bb9f3cfd74d388ee2b7fdf960536d/av-17.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:b87b98afe971cde123953073bc9c95ab0b7efd2ecc082dd2dbd11f9d9abf190e", size = 29217136, upload-time = "2026-04-18T17:12:29.189Z" }, - { url = "https://files.pythonhosted.org/packages/5f/af/0a6e1d2a845988039f6c197fa7269b5e9abbe17354fb41cc9d75bb260fcb/av-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a87a42c36e29f75e7dff7281944f2a6876a2c8875e225ccbf6c1ae62748b4caa", size = 22072676, upload-time = "2026-04-18T17:12:31.836Z" }, +version = "17.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/e3/477fa20578c284abeda08d91b63ee9abaebc93445d8feeb989d3d444bae1/av-17.1.0.tar.gz", hash = "sha256:7f1e71ff621b66253333926f948e00faae11d855b2442133c65128bca64cdeb3", size = 4288546, upload-time = "2026-06-07T05:52:55.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/92/c9d0cea4f6f8f93f5b15a39f99d2d593f922484f22a2d98a8d482283e15b/av-17.1.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:19c84fd72af5ef81a20f18fbc6f9aedff9e1455e53a7062c1d4c95926d73da4e", size = 22622703, upload-time = "2026-06-07T05:51:40.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/57/74399770aa103ee4b5ff6da1781440c91a41901d89abb2433fe88773246e/av-17.1.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:19264c9bb4bee404accc7ce9ec461f2044b7f577a70234d29aafde31ed17de46", size = 18273538, upload-time = "2026-06-07T05:51:43.078Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/27c85b12e9ffa8f3f6854358b3eabcd91f3c29c7dac36843fa1376e833f4/av-17.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:22dff0ae582d10ef08c75c2150a4fd27cfc26653b54930c7c27b9f7b3aa20723", size = 34519101, upload-time = "2026-06-07T05:51:45.305Z" }, + { url = "https://files.pythonhosted.org/packages/04/a4/542d4bfd9f4aec5f3265985b9dbc6b259d45c2e668f9714e5f4e05b71e64/av-17.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:90c49bc9608377d01e82e747377505419a229464873341db18202d5dddecce5a", size = 36647600, upload-time = "2026-06-07T05:51:48.57Z" }, + { url = "https://files.pythonhosted.org/packages/63/1e/63bd5c59580f38109fa4c452b29b715a20c9a5eb3a078b3c447484593c40/av-17.1.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cc5a5247622cb77e24c342364eb68f88c1442ddfaab60c1f1f483359d3cc7879", size = 25786289, upload-time = "2026-06-07T05:51:51.674Z" }, + { url = "https://files.pythonhosted.org/packages/70/30/78155cef0c9f8bc13f044130192c58bf962f2c9066982ff3593afe8d27f1/av-17.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff457ed419348e5b8e8c811d341389b052c5e4d5839da3794d019b125b9fe830", size = 35599848, upload-time = "2026-06-07T05:51:54.207Z" }, + { url = "https://files.pythonhosted.org/packages/76/cb/ae1d7a735a5ad9dc502dba864c51d605cbe932a769218352fd570254c38e/av-17.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1370b11a697eb3f2555906f8ab3519b0cfe48425d7830a3996ad42e6bffafda5", size = 26776479, upload-time = "2026-06-07T05:51:56.788Z" }, + { url = "https://files.pythonhosted.org/packages/fb/40/128429b9eb0c4a2beb122ed8d04b189515df68967987c2654a2e262a5c43/av-17.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3dcd41e53f53f9a3260751d9c3c11d34e93d70d61e506c81f13dbc1e3606e07b", size = 37763744, upload-time = "2026-06-07T05:51:59.222Z" }, + { url = "https://files.pythonhosted.org/packages/01/6a/5980e7bbeeadfd7a9db8e38e9f1140a3e0c392fccc31bd7b1e4a75cf5a96/av-17.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:3453b06075c7bb973fdb6de52563f7692ff05cbc64c0bb45f4fd6e8709131f2f", size = 28126516, upload-time = "2026-06-07T05:52:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/ec/87/8036b5c781bc3639ea04ef42d4e26da253bd4bd4311d8705b6a1c8824047/av-17.1.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:ad7b4aa011093324b7118245f50ac6db244cfe9900d4072508a5245a2b0d3f41", size = 22460847, upload-time = "2026-06-07T05:52:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/6d/af/dfdf6fc7b17814b50d0aa9e7a7e37b87be91be3890f44b0d525433cd1fd1/av-17.1.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:43ebbe977f19a7f2d2bd1a4e119675a0b15e05852cf7309846b6ab922ba7ffe9", size = 18159115, upload-time = "2026-06-07T05:52:06.64Z" }, + { url = "https://files.pythonhosted.org/packages/ad/13/64f6c466471cea225b8b2f4cdc51a571f8a286984b55a08d169b932fda5d/av-17.1.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6a20658ec7d96a70e14b1196eff00b7cdd8831ac3b99868e16b8ba8b24090847", size = 33224427, upload-time = "2026-06-07T05:52:09.165Z" }, + { url = "https://files.pythonhosted.org/packages/77/43/96b35170bf2e64e00a41748c6400ff73232dc0fc62ded283679fb07c7fe0/av-17.1.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f9a65d1f48b818323fb411e80358f89d77dec340b01d27c6b2dfbb9cbf4b779f", size = 35370183, upload-time = "2026-06-07T05:52:11.959Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b3/8e8b4b6498731bfbd88e8399a756543f8088f1bd33d08eab678b5aebe728/av-17.1.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:58f7593726437cda5bd19793027e027768450b5c4a594777bf487798a33db702", size = 24459265, upload-time = "2026-06-07T05:52:14.66Z" }, + { url = "https://files.pythonhosted.org/packages/14/ac/ceb84b7553db21f1143d817245c560d9267168e1e58b1a8eeae2b62c4d04/av-17.1.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:bbab058bd965309f39962e53caac8126987c68c0be094fc4f9427e5615b0218f", size = 34283709, upload-time = "2026-06-07T05:52:17.389Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/4115fd84148c9a1cf365096694be6ac882fd3cd3cdb7a2f35e71fecf1631/av-17.1.0-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9514cfda85180554c430695282faf4be3ffdf95775d8519733821244eecb58e0", size = 25397573, upload-time = "2026-06-07T05:52:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ac/92e52d5ed0e0b84d9d93e52b4338c2713d8a44082b8696e6516fdae7c4e4/av-17.1.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e1c90f85cd7431ede95b11e8e711571a896ebea433f298849c2c0f1594c8d86e", size = 36451495, upload-time = "2026-06-07T05:52:22.581Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/53a7cd34adb6a971d7e6d99663e74db286966c9db8afdca17472fdf0f98e/av-17.1.0-cp311-abi3-win_amd64.whl", hash = "sha256:5df5c1172ef1cf65a1529d612f7da7798ce2cf82c1ff7212466b538a6cc7214c", size = 28036393, upload-time = "2026-06-07T05:52:25.657Z" }, + { url = "https://files.pythonhosted.org/packages/66/47/cd9ae0edf2206351c1251bb94b5ec58728e42c5f6ee16c03c412f3a1bb3e/av-17.1.0-cp311-abi3-win_arm64.whl", hash = "sha256:ee98534242a74da847af78624779ac5a3177dc7c69f956a4da9e6f0fdb37d7f6", size = 21174601, upload-time = "2026-06-07T05:52:28.077Z" }, + { url = "https://files.pythonhosted.org/packages/36/90/b5668cddb3c401fcf22553bc495d5b0c6d8a01d118624b26f0db1d0b8653/av-17.1.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:5327807c1219293803ef0c5d1578ff3ae1cf638c09e5998962026e1a554ec240", size = 22699499, upload-time = "2026-06-07T05:52:30.335Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7e/7be6bfddb823d045ff9fd5d4deb922ee3847605e162c3882e6c45b4c35ff/av-17.1.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:6c9b71fe5c0c5a8d303b1588d4d8ce9397d6b023f467cfef95000ba1f75507fa", size = 18366696, upload-time = "2026-06-07T05:52:32.645Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/391dcfa75c1ae1977efca44b753a11b929399b558826670c16a8808dd0e3/av-17.1.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f997e3351bdf51127c07a74e21741a2996e9230cbeb2d81c14acde761b116c9c", size = 36582649, upload-time = "2026-06-07T05:52:35.218Z" }, + { url = "https://files.pythonhosted.org/packages/fb/32/7312854868b318b9d1b1dcbd1bddb460aaaeac7d57f816e11efec3bef5b1/av-17.1.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:efe9b1397300b67b644ad220c89df4892a76f2debe70f16bae1749fa20526e63", size = 38479390, upload-time = "2026-06-07T05:52:37.968Z" }, + { url = "https://files.pythonhosted.org/packages/2a/72/af47f59b4458e81ca7d89f477698dbfb3d5a0cd8ae6c1e4441d01074af8a/av-17.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:fa64e1f1500d01c4a98e7a41dc1a9a35fb4dfe71f5de0389264ec1192200c76a", size = 27127432, upload-time = "2026-06-07T05:52:40.371Z" }, + { url = "https://files.pythonhosted.org/packages/88/85/c2e6861baf0f8c7d21c4ce811d4d424fedac915e3910d3570ce4377717dc/av-17.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ffbd78d73d2c9bf31e9a007c992faec3991428b2941a3b085b84fb82e8c32d19", size = 37406592, upload-time = "2026-06-07T05:52:43.215Z" }, + { url = "https://files.pythonhosted.org/packages/ba/40/3cc13125aea976101c0858af99ac47257c0654411aa199b5d8e81eea7002/av-17.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bff8896454b38fcb785a70e5ae0485d7021cb776303a5849393128a30b8f850b", size = 28336228, upload-time = "2026-06-07T05:52:46.134Z" }, + { url = "https://files.pythonhosted.org/packages/a2/38/c7d9c3e746209a1a695c13e3aa7d817229e84a85d0a84271f313d1befdd3/av-17.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1284addf3c0dd939887a9722dc30df2241a97471ad52c3c507e31583ae22ff02", size = 39490680, upload-time = "2026-06-07T05:52:48.887Z" }, + { url = "https://files.pythonhosted.org/packages/a1/25/9d42da561b7b8f7dabdfaebba07b52977bee58c5c7e4285ac991abcfaa72/av-17.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ec630be6321b04e317862f6082e84812bbd801e55a3c2298312e3fc8a0a4af4f", size = 28355673, upload-time = "2026-06-07T05:52:51.614Z" }, + { url = "https://files.pythonhosted.org/packages/a8/41/562a61d5a61fba3ffb273a115e249f1d8471b9515c59fcc38b4b9deda238/av-17.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b41647e42884bf543b8e8d0a1dabd4d1b006c99183eb1a2d7afc5b01f73eeff4", size = 21324700, upload-time = "2026-06-07T05:52:53.972Z" }, ] [[package]] @@ -199,15 +209,15 @@ wheels = [ [[package]] name = "beautifulsoup4" -version = "4.14.3" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, ] [[package]] @@ -221,30 +231,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.9" +version = "1.43.28" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/cc/42d798fc5305e4636170b50cdfb305ff0a81f470e35131f4a0d2641976ae/boto3-1.43.9.tar.gz", hash = "sha256:37dac72f2921095378c0200caf07918d5e10a82b7c1f611abb70e44f69d0b962", size = 113135, upload-time = "2026-05-15T19:28:31.167Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/f2/a976b2a81d8dc7ff675f4b614367a185727061130184a28da0f53f446b97/boto3-1.43.28.tar.gz", hash = "sha256:8391fdcc4d8e1d4e0bf96575a7e5610964a4d401dafa4dccb0a5bade8dd3fbb0", size = 113202, upload-time = "2026-06-11T19:29:01.464Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/dc/51286e9551f7852a79ce5d2a57468d9d905c30d32bcace55204551db202d/boto3-1.43.9-py3-none-any.whl", hash = "sha256:5e967292d361482793471bd80fad1e714515b7401f65a0d5b4aa6ef9d009c030", size = 140523, upload-time = "2026-05-15T19:28:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a5/47db150ea6380f11569b87d3ad064e3c929e5abe227a549d472fab6f5f3a/boto3-1.43.28-py3-none-any.whl", hash = "sha256:4fe6df2163aea02b561eca0d685e2f41a059d71f03721a3e79c3b522e79a3b56", size = 140536, upload-time = "2026-06-11T19:29:00.143Z" }, ] [[package]] name = "botocore" -version = "1.43.9" +version = "1.43.28" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/e8/f696c80982685a4cdb3df5f0781919afa50262f40e1aac7066c9c2520deb/botocore-1.43.9.tar.gz", hash = "sha256:93e91c7160678182860f5902ee4cfe6d643cac0d9ee84d3eb65becc9f4c00228", size = 15357963, upload-time = "2026-05-15T19:28:19.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/dc/1b01808003f88f8a328732c979f20cb0456791048b4440fc4abcae08c1a0/botocore-1.43.28.tar.gz", hash = "sha256:9bbad501a68e4ffdbeff76a382507f5d7827abc316f34a218ab76f5293e6c78d", size = 15503514, upload-time = "2026-06-11T19:28:50.989Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/c9/a1b51a74d476f5cb2f555ce8274f0f6b9fb21d75cc3f57b87dd0632ee17a/botocore-1.43.9-py3-none-any.whl", hash = "sha256:b9bdcd9c87fc552aad30006f00167d9ebb3480e1b06f1902bac5b2c41014fdab", size = 15039827, upload-time = "2026-05-15T19:28:14.543Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8c/14916c353ce8a29d14cf6308c2bef842bbb25dde6defc620e26e28063331/botocore-1.43.28-py3-none-any.whl", hash = "sha256:8147adea89b4c9324e842cd8c01ea1a0e17c92cb6ebeaa8cb774f821cb5a7629", size = 15188401, upload-time = "2026-06-11T19:28:47.244Z" }, ] [[package]] @@ -283,11 +293,11 @@ filecache = [ [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.5.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, ] [[package]] @@ -488,14 +498,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.3" +version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, ] [[package]] @@ -582,14 +592,9 @@ name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.13'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ @@ -673,115 +678,115 @@ wheels = [ [[package]] name = "coverage" -version = "7.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/9d/7c83ef51c3eb495f10010094e661833588b7709946da634c8b66520b97c7/coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075", size = 219668, upload-time = "2026-05-10T17:59:23.106Z" }, - { url = "https://files.pythonhosted.org/packages/24/34/898546aefbd28f0af131201d0dc852c9e976f817bd7d5bfb8dc4e02863bb/coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82", size = 220192, upload-time = "2026-05-10T17:59:26.095Z" }, - { url = "https://files.pythonhosted.org/packages/df/4a/b457c88aca72b0df13a98167ebd5d947135ccd9881ea88ce6a570e13aa9b/coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c", size = 246932, upload-time = "2026-05-10T17:59:27.806Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d9/92600e89486fd074c50f0117422b2c9592c3e144e2f25bd5ac0bc62bc7a0/coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893", size = 248762, upload-time = "2026-05-10T17:59:29.479Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e1/9ea1eb9c311da7f15853559dc1d9d82bef88ecd3e59fbeb51f16bc2ffa91/coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20", size = 250625, upload-time = "2026-05-10T17:59:31.33Z" }, - { url = "https://files.pythonhosted.org/packages/a5/03/57afca1b8106f8549a5329139315041fe166d6099bd9381346b9430dfbd1/coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec", size = 252539, upload-time = "2026-05-10T17:59:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/57/5e/2e9fc63c9928119c1dbae02222be51407d3e7ebac5811ebbda4af3557795/coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757", size = 247636, upload-time = "2026-05-10T17:59:34.599Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e2/0b7898cda21041cc67546e19b80ba66cbbb47cbece52a76a5904de6a3aaf/coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a", size = 248666, upload-time = "2026-05-10T17:59:36.232Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/d33662a2fdaef23229c15921f39c84ec38441f3069ba26e134ed402c833b/coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea", size = 246670, upload-time = "2026-05-10T17:59:38.029Z" }, - { url = "https://files.pythonhosted.org/packages/99/b2/533942c3bfbf6770b5c32d7f2ff029fe013dba31f3fe8b45cabbb250365e/coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb", size = 250484, upload-time = "2026-05-10T17:59:39.974Z" }, - { url = "https://files.pythonhosted.org/packages/d8/00/15acbad83a96de13c73831486c7627bfed73dfaec53b04e4a6315edf3fd8/coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218", size = 246942, upload-time = "2026-05-10T17:59:41.659Z" }, - { url = "https://files.pythonhosted.org/packages/70/db/cef0228de493f2c740c760a9057a61d00c6849480073b70a75b87c7d4bab/coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85", size = 247544, upload-time = "2026-05-10T17:59:43.471Z" }, - { url = "https://files.pythonhosted.org/packages/77/a0/d9ef8e148f3025c2ae8401d77cda1502b6d2a4d8102603a8af31460aedb6/coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323", size = 222285, upload-time = "2026-05-10T17:59:44.908Z" }, - { url = "https://files.pythonhosted.org/packages/85/c0/30c454c7d3cf47b2805d4e06f12443f5eece8a5d030d3b0350e7b74ecb49/coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a", size = 223215, upload-time = "2026-05-10T17:59:46.779Z" }, - { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, - { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633, upload-time = "2026-05-10T17:59:53.244Z" }, - { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743, upload-time = "2026-05-10T17:59:55.021Z" }, - { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700, upload-time = "2026-05-10T17:59:56.511Z" }, - { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854, upload-time = "2026-05-10T17:59:57.94Z" }, - { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433, upload-time = "2026-05-10T17:59:59.688Z" }, - { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494, upload-time = "2026-05-10T18:00:01.953Z" }, - { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261, upload-time = "2026-05-10T18:00:03.779Z" }, - { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216, upload-time = "2026-05-10T18:00:05.262Z" }, - { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125, upload-time = "2026-05-10T18:00:06.858Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300, upload-time = "2026-05-10T18:00:08.608Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241, upload-time = "2026-05-10T18:00:10.746Z" }, - { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908, upload-time = "2026-05-10T18:00:12.242Z" }, - { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, - { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, - { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, - { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, - { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, - { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, - { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, - { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, - { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, - { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, - { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, - { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, - { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, - { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, - { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, - { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, - { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, - { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, - { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, - { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, - { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, - { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, - { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, - { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, - { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, - { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, - { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, - { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, - { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, - { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, - { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, - { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, - { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, - { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, - { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, - { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, - { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, - { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, - { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, - { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, - { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, - { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, - { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, - { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, - { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, - { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, - { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, - { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, - { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, - { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, - { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, - { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, - { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, - { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, - { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, +version = "7.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/69/0d2ef01ff4b8fcecd4cba920d11e92fa4f96ae412441d3b56a90a258e69b/coverage-7.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e3680291c4a1d0dadfa84a2c459576a4af5133abb617905714339a0c73138cf", size = 219722, upload-time = "2026-05-26T20:38:14.002Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ae/9afdeaa31b9d9ce98124b6abf8bb49119bf71aecae04f8567c189d91299f/coverage-7.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5274669f37f2343635a347b91a60777621341ab3378e9c6ac9335eee704bddf", size = 220240, upload-time = "2026-05-26T20:38:17.424Z" }, + { url = "https://files.pythonhosted.org/packages/51/69/c998589871df7ea7dba865cc5ee32b5a3e1d47ba6c68ef91104c7c46fa5e/coverage-7.14.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe5a5fec635799ef33428f1e5e61bafa45a92a96190ba731561ba558ccc214d", size = 246981, upload-time = "2026-05-26T20:38:19.266Z" }, + { url = "https://files.pythonhosted.org/packages/fc/10/1c7d04c13040dac531d21b712bbe08f902e6dd9b58f5d77875c4d030f8f2/coverage-7.14.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62a9f70b52e0b5a95cfef4a5c5641b06983cadc5e538a3feeb5c00211f523ac2", size = 248812, upload-time = "2026-05-26T20:38:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/2a38a4607ef27cadcfbcee034dba5830ae2569f90144a0f4c7dbf47d30b0/coverage-7.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c18ebc343e15be53049b3a2dce38fe82d58f37e20ab9094b3a39c0aa4f6bb47", size = 250675, upload-time = "2026-05-26T20:38:22.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a2/a446ed9752a4a59b79e0fb6cbb319f6facb2183045c0725462625e66f87e/coverage-7.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b84ffdf877644e7096aa936991efeed873f7f3df57b9cd001312b7668ab08550", size = 252590, upload-time = "2026-05-26T20:38:23.63Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fd/e81fbd7ba752365546e9842b1cbdaad3d6919d2a522c590aef16a281ec5e/coverage-7.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e854312c4103f2ad4c0dc023b69b77ebfd2c89db5f86c4c94dc2353f9a92167e", size = 247691, upload-time = "2026-05-26T20:38:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/53/35/f3c26fdaae9ea937d154ca4d372e5ea0a4167ff70d36c6074ac2eacb2f83/coverage-7.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c643734307300234fafa36bf2a040a7235f8f177ea1fd6ec1423aea6fb7b929f", size = 248716, upload-time = "2026-05-26T20:38:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/2e/14/940b6c49551fd343e8507ee2b0ba7af5d0aa04ed5bf768285cb7c72a9884/coverage-7.14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84ac9499e48700399a5dd0ea7085b5091961fec52c68d66b4ec0d3cf7f4441b1", size = 246721, upload-time = "2026-05-26T20:38:28.282Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2c/40fc0634186c28292a662dff578866b3913983d6c375a3c2a74020938719/coverage-7.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7f02d09f70776579b926d889a4c9c235070a1f47c40458aeaca563fae5acfdb5", size = 250533, upload-time = "2026-05-26T20:38:29.753Z" }, + { url = "https://files.pythonhosted.org/packages/de/e3/2c26bf1e811f9df991ff2a9bdddebdd13ee0665d564df7d05979f9146297/coverage-7.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ce66d8e46da2bb5ee313a745cbd2e391d319176c1f7a9451bfcd3a2fb920859b", size = 246990, upload-time = "2026-05-26T20:38:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b0/060260ef56bd92363ebdce0c7095ce422b06e69aae71828efeca473ab1ca/coverage-7.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c912c259304cfb5ee584481cfb7ce1ff932b4d61e6c9140b8f19cb7b5ed82332", size = 247593, upload-time = "2026-05-26T20:38:33.065Z" }, + { url = "https://files.pythonhosted.org/packages/63/f3/501502046efeb0d6d94b5ca54941d95f1184183dd6bdb7f283985783bb4a/coverage-7.14.1-cp310-cp310-win32.whl", hash = "sha256:1238cb94638e610e972c60dac68e813f868dc7d6e982535270558443058d9d59", size = 222330, upload-time = "2026-05-26T20:38:35.36Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5d/1bf99f2c558f128faf7906817ccbdb576ba815d3b41ce2ac1719b70a3663/coverage-7.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253", size = 223261, upload-time = "2026-05-26T20:38:37.196Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/477ad149490e6cb849f28abea1dabb9c823cea72e7500c81b4240ce619c0/coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f", size = 219848, upload-time = "2026-05-26T20:38:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/91/82/a5eb47257c50601bb7b9a9d2857c67b7a3a85ad74180eb2c98bb1fbe0ce5/coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4", size = 220354, upload-time = "2026-05-26T20:38:40.232Z" }, + { url = "https://files.pythonhosted.org/packages/43/8b/78419b5391a5cb706b6544390507e469d83ffc9a8248b02c4011aceb9365/coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1", size = 250771, upload-time = "2026-05-26T20:38:41.782Z" }, + { url = "https://files.pythonhosted.org/packages/77/63/e77aaacd491182210d639636b7a8bba23ffffa9b82aa3762da9431855fa9/coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f", size = 252683, upload-time = "2026-05-26T20:38:43.305Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/a022e3cfbec2ac241640003cb3a817e161d9c7f5aa9b49173756cdc03204/coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129", size = 254791, upload-time = "2026-05-26T20:38:45.361Z" }, + { url = "https://files.pythonhosted.org/packages/61/d6/967e408aca4c1ceb88cb0cc677169110ae7f5995fb5eaf5fb1f5a1bb8f5d/coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860", size = 256748, upload-time = "2026-05-26T20:38:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/869188f7fe28638078ec479331ace6dc5f7b40b7153eb616f47ab79404d8/coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c", size = 250907, upload-time = "2026-05-26T20:38:48.493Z" }, + { url = "https://files.pythonhosted.org/packages/07/aa/adb7d3b4278d690e68703abcd76ab1b948242e3668d921711551b78f9ddb/coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7", size = 252483, upload-time = "2026-05-26T20:38:50.074Z" }, + { url = "https://files.pythonhosted.org/packages/43/61/331c74103c62dcb0c4b9b3a0de9a61aca016208b0a90f109592a9f9ecc28/coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec", size = 250545, upload-time = "2026-05-26T20:38:51.613Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/c5dae3c104d89be04828f61810e6b3473825482e4c288cc4ed04553e08ae/coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef", size = 254310, upload-time = "2026-05-26T20:38:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a1/2b9d5863e3b83c01ad8199e3c597802fbb3a9dc90b058885804c20296d31/coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df", size = 250266, upload-time = "2026-05-26T20:38:55.414Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/0e511fbdb269359be26fe678a1c3fa1f2aa2a01573cc3f54268c8d6d4797/coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9", size = 251174, upload-time = "2026-05-26T20:38:57.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/10/e55307b622b3dd9671cb321824502dc10f93e72f2802b9946159a8edadeb/coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548", size = 222354, upload-time = "2026-05-26T20:38:58.727Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/107421693cfb71e4f1ca5bf70443f64d4161878068d07a3e51c7ad21d17b/coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e", size = 223290, upload-time = "2026-05-26T20:39:00.413Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1d/3e3644585eb29e9dafefb19555078529a4d7cce12bd21929664eea989277/coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3", size = 221953, upload-time = "2026-05-26T20:39:02.159Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" }, + { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" }, + { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" }, + { url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" }, + { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" }, + { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" }, + { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" }, + { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" }, + { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" }, + { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" }, + { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" }, + { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" }, + { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" }, + { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" }, + { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" }, + { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" }, + { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" }, + { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" }, + { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" }, + { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, ] [package.optional-dependencies] @@ -791,67 +796,67 @@ toml = [ [[package]] name = "cryptography" -version = "48.0.0" +version = "48.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, - { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, - { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, - { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, - { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, - { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" }, + { url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" }, + { url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" }, + { url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" }, + { url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" }, + { url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" }, + { url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" }, + { url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" }, + { url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" }, + { url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" }, + { url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" }, + { url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" }, + { url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" }, + { url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" }, + { url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" }, + { url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" }, + { url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/eb4e394e587341fdad09a09101fa76478ead3a78b0ad63e55c22f0d75c02/cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a", size = 3951747, upload-time = "2026-06-09T22:31:23.871Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/3f43451b4f858bfceaaaffc649e6e787e8d4fb332a1d443af39ab02cc8f1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd", size = 4641226, upload-time = "2026-06-09T22:31:02.532Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/855584c2c23b09e4ce2d3b9c30e983e679cd60b068c513c6bbdb91e11782/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c", size = 4668958, upload-time = "2026-06-09T22:32:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/42/3b/d35750e41d803d1e516fd6d6011f065424924da7af1748cef4cc9cb3ede1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9", size = 4640793, upload-time = "2026-06-09T22:32:26.331Z" }, + { url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8c/ce3823c06c2804f194f9e64f0d67fa3f4094a39f2bb1a990cd03603af8fc/cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a", size = 3742204, upload-time = "2026-06-09T22:31:34.773Z" }, ] [[package]] name = "ctranslate2" -version = "4.7.1" +version = "4.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, @@ -860,36 +865,36 @@ dependencies = [ { name = "setuptools", marker = "python_full_version >= '3.11'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/e0/b69c40c3d739b213a78d327071240590792071b4f890e34088b03b95bb1e/ctranslate2-4.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9017a355dd7c6d29dc3bca6e9fc74827306c61b702c66bb1f6b939655e7de3fa", size = 1255773, upload-time = "2026-02-04T06:11:04.769Z" }, - { url = "https://files.pythonhosted.org/packages/51/29/e5c2fc1253e3fb9b2c86997f36524bba182a8ed77fb4f8fe8444a5649191/ctranslate2-4.7.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:6abcd0552285e7173475836f9d133e04dfc3e42ca8e6930f65eaa4b8b13a47fa", size = 11914945, upload-time = "2026-02-04T06:11:06.853Z" }, - { url = "https://files.pythonhosted.org/packages/03/25/e7fe847d3f02c84d2e9c5e8312434fbeab5af3d8916b6c8e2bdbe860d052/ctranslate2-4.7.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8492cba605319e0d7f2760180957d5a2a435dfdebcef1a75d2ade740e6b9fb0b", size = 16547973, upload-time = "2026-02-04T06:11:09.021Z" }, - { url = "https://files.pythonhosted.org/packages/68/75/074ed22bc340c2e26c09af6bf85859b586516e4e2d753b20189936d0dcf7/ctranslate2-4.7.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:688bd82482b5d057eff5bc1e727f11bb9a1277b7e4fce8ab01fd3bb70e69294b", size = 38636471, upload-time = "2026-02-04T06:11:12.146Z" }, - { url = "https://files.pythonhosted.org/packages/76/b6/9baf8a565f6dcdbfbc9cfd179dd6214529838cda4e91e89b616045a670f0/ctranslate2-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3b39a5f4e3c87ac91976996458a64ba08a7cbf974dc0be4e6df83a9e040d4bd2", size = 18842389, upload-time = "2026-02-04T06:11:15.154Z" }, - { url = "https://files.pythonhosted.org/packages/da/25/41920ccee68e91cb6fa0fc9e8078ab2b7839f2c668f750dc123144cb7c6e/ctranslate2-4.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f74200bab9996b14a57cf6f7cb27d0921ceedc4acc1e905598e3e85b4d75b1ec", size = 1256943, upload-time = "2026-02-04T06:11:17.781Z" }, - { url = "https://files.pythonhosted.org/packages/79/22/bc81fcc9f10ba4da3ffd1a9adec15cfb73cb700b3bbe69c6c8b55d333316/ctranslate2-4.7.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:59b427eb3ac999a746315b03a63942fddd351f511db82ba1a66880d4dea98e25", size = 11916445, upload-time = "2026-02-04T06:11:19.938Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a7/494a66bb02c7926331cadfff51d5ce81f5abfb1e8d05d7f2459082f31b48/ctranslate2-4.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95f0c1051c180669d2a83a44b44b518b2d1683de125f623bbc81ad5dd6f6141c", size = 16696997, upload-time = "2026-02-04T06:11:22.697Z" }, - { url = "https://files.pythonhosted.org/packages/ed/4e/b48f79fd36e5d3c7e12db383aa49814c340921a618ef7364bd0ced670644/ctranslate2-4.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed92d9ab0ac6bc7005942be83d68714c80adb0897ab17f98157294ee0374347", size = 38836379, upload-time = "2026-02-04T06:11:26.325Z" }, - { url = "https://files.pythonhosted.org/packages/d2/23/8c01ac52e1f26fc4dbe985a35222ae7cd365bbf7ee5db5fd5545d8926f91/ctranslate2-4.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:67d9ad9b69933fbfeee7dcec899b2cd9341d5dca4fdfb53e8ba8c109dc332ee1", size = 18843315, upload-time = "2026-02-04T06:11:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/fc/0f/581de94b64c5f2327a736270bc7e7a5f8fe5cf1ed56a2203b52de4d8986a/ctranslate2-4.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4c0cbd46a23b8dc37ccdbd9b447cb5f7fadc361c90e9df17d82ca84b1f019986", size = 1257089, upload-time = "2026-02-04T06:11:32.442Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e9/d55b0e436362f9fe26bd98fefd2dd5d81926121f1d7f799c805e6035bb26/ctranslate2-4.7.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:5b141ddad1da5f84cf3c2a569a56227a37de649a555d376cbd9b80e8f0373dd8", size = 11918502, upload-time = "2026-02-04T06:11:33.986Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ce/9f29f0b0bb4280c2ebafb3ddb6cdff8ef1c2e185ee020c0ec0ecba7dc934/ctranslate2-4.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d00a62544db4a3caaa58a3c50d39b25613c042b430053ae32384d94eb1d40990", size = 16859601, upload-time = "2026-02-04T06:11:36.227Z" }, - { url = "https://files.pythonhosted.org/packages/b3/86/428d270fd72117d19fb48ed3211aa8a3c8bd7577373252962cb634e0fd01/ctranslate2-4.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:722b93a89647974cbd182b4c7f87fefc7794fff7fc9cbd0303b6447905cc157e", size = 38995338, upload-time = "2026-02-04T06:11:42.789Z" }, - { url = "https://files.pythonhosted.org/packages/4a/f4/d23dbfb9c62cb642c114a30f05d753ba61d6ffbfd8a3a4012fe85a073bcb/ctranslate2-4.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f734dc3757118094663bdaaf713f5090c55c1927fb330a76bb8b84173940e8", size = 18844949, upload-time = "2026-02-04T06:11:45.436Z" }, - { url = "https://files.pythonhosted.org/packages/34/6d/eb49ba05db286b4ea9d5d3fcf5f5cd0a9a5e218d46349618d5041001e303/ctranslate2-4.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b2abf2929756e3ec6246057b56df379995661560a2d776af05f9d97f63afcf5", size = 1256960, upload-time = "2026-02-04T06:11:47.487Z" }, - { url = "https://files.pythonhosted.org/packages/45/5a/b9cce7b00d89fc6fdeaf27587aa52d0597b465058563e93ff50910553bdd/ctranslate2-4.7.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:857ef3959d6b1c40dc227c715a36db33db2d097164996d6c75b6db8e30828f52", size = 11918645, upload-time = "2026-02-04T06:11:49.599Z" }, - { url = "https://files.pythonhosted.org/packages/ea/03/c0db0a5276599fb44ceafa2f2cb1afd5628808ec406fe036060a39693680/ctranslate2-4.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:393a9e7e989034660526a2c0e8bb65d1924f43d9a5c77d336494a353d16ba2a4", size = 16860452, upload-time = "2026-02-04T06:11:52.276Z" }, - { url = "https://files.pythonhosted.org/packages/0b/03/4e3728ce29d192ee75ed9a2d8589bf4f19edafe5bed3845187de51b179a3/ctranslate2-4.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a3d0682f2b9082e31c73d75b45f16cde77355ab76d7e8356a24c3cb2480a6d3", size = 38995174, upload-time = "2026-02-04T06:11:55.477Z" }, - { url = "https://files.pythonhosted.org/packages/9b/15/6e8e87c6a201d69803a79ac2e29623ce7c2cc9cd1df9db99810cca714373/ctranslate2-4.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:baa6d2b10f57933d8c11791e8522659217918722d07bbef2389a443801125fe7", size = 18844953, upload-time = "2026-02-04T06:11:58.519Z" }, - { url = "https://files.pythonhosted.org/packages/fd/73/8a6b7ba18cad0c8667ee221ddab8c361cb70926440e5b8dd0e81924c28ac/ctranslate2-4.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d5dfb076566551f4959dfd0706f94c923c1931def9b7bb249a2caa6ab23353a0", size = 1257560, upload-time = "2026-02-04T06:12:00.926Z" }, - { url = "https://files.pythonhosted.org/packages/70/c2/8817ca5d6c1b175b23a12f7c8b91484652f8718a76353317e5919b038733/ctranslate2-4.7.1-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:eecdb4ed934b384f16e8c01b185b082d6b5ffc7dcbb0b6a6eb48cd465282d957", size = 11918995, upload-time = "2026-02-04T06:12:02.875Z" }, - { url = "https://files.pythonhosted.org/packages/ac/33/b8eb3acc67bbca4d9872fc9ff94db78e6167a7ba5cd932f585d1560effc7/ctranslate2-4.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1aa6796edcc3c8d163c9e39c429d50076d266d68980fed9d1b2443f617c67e9e", size = 16844162, upload-time = "2026-02-04T06:12:05.099Z" }, - { url = "https://files.pythonhosted.org/packages/80/11/6474893b07121057035069a0a483fe1cd8c47878213f282afb4c0c6fc275/ctranslate2-4.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24c0482c51726430fb83724451921c0e539d769c8618dcfd46b1645e7f75960d", size = 38966728, upload-time = "2026-02-04T06:12:07.923Z" }, - { url = "https://files.pythonhosted.org/packages/94/88/8fc7ff435c5e783e5fad9586d839d463e023988dbbbad949d442092d01f1/ctranslate2-4.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:76db234c0446a23d20dd8eeaa7a789cc87d1d05283f48bf3152bae9fa0a69844", size = 19100788, upload-time = "2026-02-04T06:12:10.592Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b3/f100013a76a98d64e67c721bd4559ea4eeb54be3e4ac45f4d801769899af/ctranslate2-4.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:058c9db2277dc8b19ecc86c7937628f69022f341844b9081d2ab642965d88fc6", size = 1280179, upload-time = "2026-02-04T06:12:12.596Z" }, - { url = "https://files.pythonhosted.org/packages/39/22/b77f748015667a5e2ca54a5ee080d7016fce34314f0e8cf904784549305a/ctranslate2-4.7.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:5abcf885062c7f28a3f9a46be8d185795e8706ac6230ad086cae0bc82917df31", size = 11940166, upload-time = "2026-02-04T06:12:14.054Z" }, - { url = "https://files.pythonhosted.org/packages/7d/78/6d7fd52f646c6ba3343f71277a9bbef33734632949d1651231948b0f0359/ctranslate2-4.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9950acb04a002d5c60ae90a1ddceead1a803af1f00cadd9b1a1dc76e1f017481", size = 16849483, upload-time = "2026-02-04T06:12:17.082Z" }, - { url = "https://files.pythonhosted.org/packages/40/27/58769ff15ac31b44205bd7a8aeca80cf7357c657ea5df1b94ce0f5c83771/ctranslate2-4.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1dcc734e92e3f1ceeaa0c42bbfd009352857be179ecd4a7ed6cccc086a202f58", size = 38949393, upload-time = "2026-02-04T06:12:21.302Z" }, - { url = "https://files.pythonhosted.org/packages/0e/5c/9fa0ad6462b62efd0fb5ac1100eee47bc96ecc198ff4e237c731e5473616/ctranslate2-4.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:dfb7657bdb7b8211c8f9ecb6f3b70bc0db0e0384d01a8b1808cb66fe7199df59", size = 19123451, upload-time = "2026-02-04T06:12:24.115Z" }, + { url = "https://files.pythonhosted.org/packages/36/6b/7329ff26a4bdfb1b395cf4dfb2b057ebe3881c5fe91f2022634689754fb8/ctranslate2-4.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e66b5dc33e94a05dfe0fcdf6ab17dfe6e0335017cc6a315de044214063b1c533", size = 1268221, upload-time = "2026-06-06T19:17:27.597Z" }, + { url = "https://files.pythonhosted.org/packages/db/3e/4e3289f428f51bbf12fd77a708a430dc1792375bd924c2c7cbaecc6e6d83/ctranslate2-4.8.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f25566e056d1fa9da47d6e374b2d04d6a6dfa0b631e52687a0bc4101633ecb53", size = 11925104, upload-time = "2026-06-06T19:17:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/b220fc42f38b8fd8f938d6621436fceea136cb5aad4c6a6f9f125e2755ba/ctranslate2-4.8.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65e64a5d79de82f302677b7ac07d3a540f893657c33fb0196824db87a17e4900", size = 16554877, upload-time = "2026-06-06T19:17:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/7a/85/d562b84d31ec28f5ddf1b49444d706b033075d42aaf656b8afea3120468f/ctranslate2-4.8.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4c8b9dc6bd8a3e79fa4109fa918e1c563b533e84a1e6c96eda6ff57cfee17f4", size = 39150038, upload-time = "2026-06-06T19:17:37.677Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b7/f6f6c3e5c175b10a48e949bc46c42a58f88647702ce1975e29fd6e45a5e2/ctranslate2-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b55976b0248d62aacce4e3569b0555ec2861d77b8b334ed03eaf757f51d0492d", size = 19216424, upload-time = "2026-06-06T19:17:41.401Z" }, + { url = "https://files.pythonhosted.org/packages/70/92/ae797ea2def987a0496319c5d8988cd4aaf11a6c0c71a2fd9bbb75d13f1f/ctranslate2-4.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f56f8de6e6e036a306d427b86d86964076b614e2358b923f93fa160139ac6f5", size = 1269068, upload-time = "2026-06-06T19:17:43.67Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/ed546dd5ba660c80d83a25731313956b417d35152424f92f543aec093d0e/ctranslate2-4.8.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:59c71320788b88621be143f2795048e9f510ff690c549cffc2827831f85a1a04", size = 11926418, upload-time = "2026-06-06T19:17:45.741Z" }, + { url = "https://files.pythonhosted.org/packages/df/ef/ab22bfafc13c5d2c5a3bbbcf89ccb140a365250df29e3226dff0cfbb6748/ctranslate2-4.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2676854f374e6720600467cbde2ea2ea844fb0b6fb3e8a79795d495a3bdc7469", size = 16705990, upload-time = "2026-06-06T19:17:48.441Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c6/29d9100520d586fc5e5142ff17b2d28e4b9beeafc196982395497700fee2/ctranslate2-4.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83336d60ae04f19a30a90405040efaea1dfa0e1d95c2fe1513e53dade4681c85", size = 39349218, upload-time = "2026-06-06T19:17:52.9Z" }, + { url = "https://files.pythonhosted.org/packages/4c/2b/486dc27e200f905f3acc50ed20000ee714097616f8fe66585c29c8a4b26a/ctranslate2-4.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:402472d283d844579961b8522589401bc2c50f77f1b820783b01c25060260c3c", size = 19217441, upload-time = "2026-06-06T19:17:58.36Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f8/871b866c10d4fe4479866c4aa9c6a7ba4073dc2a657879d44411b2fb8f4c/ctranslate2-4.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:94ec37527dd815531209694854dd5177e763ed51d35b4b2c34da3c3ad2c9b9fd", size = 1269020, upload-time = "2026-06-06T19:18:00.599Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ea/316e3df68e21f79e20c277bf5c65d9825a42484ed7e3df2e6e325275ea5f/ctranslate2-4.8.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:f0b93d127a4efb6481e3d0da4c3a6ac9889a8e9d8b50f9930bc5b2401fe5e598", size = 11928718, upload-time = "2026-06-06T19:18:02.767Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d1/c4234eea5fe84733c0faed486881c7b3ebf9bc1351cb96cde7b0ecc78198/ctranslate2-4.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:edfa0c1b348525d6c2713a53c90c0c50ae7a7bb2e4d59d8a59150aadba818991", size = 16880797, upload-time = "2026-06-06T19:18:05.425Z" }, + { url = "https://files.pythonhosted.org/packages/ea/34/a0ac6e2538b7d730e4537cd01ded7817dda9bc97f5b6161bbd52d16e70a3/ctranslate2-4.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:247efccc2da9a63e8bf22abb4e87789f44ec1454bdcb227b07860cdc826fc89a", size = 39526315, upload-time = "2026-06-06T19:18:09.344Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/2c3c7b110c48c36d024c5247195f2ad4fc1e34cbf482dab62ccb3898cb70/ctranslate2-4.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:06feaafe134aafa8cb2fb1fdb82e36f050bb05929dfde1a95f4fe4d7881dfc76", size = 19218985, upload-time = "2026-06-06T19:18:12.742Z" }, + { url = "https://files.pythonhosted.org/packages/db/59/ef2b2b0c2624122a46aed4300aaa293f83fb8cbd02543eeb4fc30e9b49ce/ctranslate2-4.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:892898271f7f2c6a3651a7ea4d91f5727f97baed36171e5026456dbf0916030a", size = 1268988, upload-time = "2026-06-06T19:18:15.016Z" }, + { url = "https://files.pythonhosted.org/packages/6a/04/7295ef780fa7cb3e0835cb5b4032a5470ca484cd4c719ac01d6665169e07/ctranslate2-4.8.0-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:aa53859acd61db1e287db57be955885fd271f84348af37dabe2c3dbd0b4425e2", size = 11928594, upload-time = "2026-06-06T19:18:16.938Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f0/23e4e7d422fe1904db8389dd2a979c8e278e2a0d56064bb54e7651372b18/ctranslate2-4.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7531ed5148099792ec488f80e21f9bb551fb9fdf335c4b6c3720f11d53f2639c", size = 16881562, upload-time = "2026-06-06T19:18:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/282e00f63bc1ceb45d9f0e6cb11a9f63e3f8befd980e6264e7b6b81d1536/ctranslate2-4.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3427639aa036ea3d8e6864598552dc0b32c4017801925b8165638acb937b39cf", size = 39526333, upload-time = "2026-06-06T19:18:25.383Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3c7e20503d008c62fae22c1ebe7bd4ce3c50b3033e7d97ba04c8c92e6dd1/ctranslate2-4.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:f6ab3fa77a0a4259deaf7f23979c9182974d45857873211d417fdf364a55cac5", size = 19218975, upload-time = "2026-06-06T19:18:28.506Z" }, + { url = "https://files.pythonhosted.org/packages/d2/4b/24bc25460cfb36ca441e4c532d8a5bc89d84921d2af9353c4aa711cff0be/ctranslate2-4.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0ccb10af5b7ef4747d5f275e11d809ebf9057c7b1e5241fdbf386b120eaf353d", size = 1269827, upload-time = "2026-06-06T19:18:31.328Z" }, + { url = "https://files.pythonhosted.org/packages/91/29/9fb73d9a124b3d2b44f1cf28b73ddbe793adbeb5b7366b258812fce9ff33/ctranslate2-4.8.0-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:d99095855b4f7921be85e694e20cce6a834f7d4464335150186efdf6430a3b58", size = 11929083, upload-time = "2026-06-06T19:18:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/df/8a/266ef810e2567fc4aaf322277800eff2c117a85dafcb42f08b76efc69913/ctranslate2-4.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0465bb50ddf143d02d52ea818c25a49ef5c2395c3fff98f6a905e239946cd765", size = 16870501, upload-time = "2026-06-06T19:18:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d1/ae7f0f1b4e40a00546d3f42749ae1504aaf6658e262a5c6f01269c1cae2d/ctranslate2-4.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e14648a8bed404f27d2fad5c4dbcf6eae5e584cc9641400fa443382057200c8d", size = 39491984, upload-time = "2026-06-06T19:18:40.24Z" }, + { url = "https://files.pythonhosted.org/packages/ff/11/be1a32f2691c5508db978937f078a296d184debf5ea03c056b2f90b0f66e/ctranslate2-4.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:1a758c95aa8f9bdaec476433515a00695b453b1315a0664612fae25b75818d44", size = 19475708, upload-time = "2026-06-06T19:18:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/68/4d10a76d9920d72ae2bba6e1601cf1ceecc06b26ad038e484ca373f0d949/ctranslate2-4.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:68405a3f88b6ec89c9768c59fded6d212b335d39c67cf0ccf058a906188a282c", size = 1292444, upload-time = "2026-06-06T19:18:46.879Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a5/aebac79c12c0bc3f0f55b11e56e7d7e8060f84bc69239f8a4b2a46ca9630/ctranslate2-4.8.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:39d36c67554afbed8f8cd8c03bcfb3d3453f02e96b2b6d5a0e7391daec81358d", size = 11950137, upload-time = "2026-06-06T19:18:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/a0/21/269e389172046db797992454f26e66d8921508b3a92d7ef2bd0184b5db51/ctranslate2-4.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4791301710d8b6e1078106d4ba34e3a176f4be19f7047de06b6935b15d422bd", size = 16858464, upload-time = "2026-06-06T19:18:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/df/da/f8c28d26c9dae58042b547719295f1afc544ad31829f0d5e7fe111937d12/ctranslate2-4.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e742074822e5b27fa5b991b5ab785e6a8ae5909996023e798448fa7df478dea0", size = 39462450, upload-time = "2026-06-06T19:18:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/e6/cd/64af7f2416be21cf8fd8ea3dbdb8e757f3b4f81b629ad43613729c5e0bd9/ctranslate2-4.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ddc359f9b886e0f92bd3061038711285e0003dd69e7f8184cbcd29322a832458", size = 19498147, upload-time = "2026-06-06T19:19:00.4Z" }, ] [[package]] @@ -903,7 +908,7 @@ wheels = [ [[package]] name = "cyclonedx-python-lib" -version = "11.7.0" +version = "11.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "license-expression" }, @@ -912,9 +917,9 @@ dependencies = [ { name = "sortedcontainers" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/0d/64f02d3fd9c116d6f50a540d04d1e4f2e3c487f5062d2db53733ddb25917/cyclonedx_python_lib-11.7.0.tar.gz", hash = "sha256:fb1bc3dedfa31208444dbd743007f478ab6984010a184e5bd466bffd969e936e", size = 1411174, upload-time = "2026-03-17T15:19:16.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/54/40d741cb605229cddcf9ec689b0fd401e39e2e70c2fe9cc728923b983b8e/cyclonedx_python_lib-11.10.0.tar.gz", hash = "sha256:d03d6ea271e26feaf123b8b1b34468a305f33a338c5763f56e397a8408f9b290", size = 1429036, upload-time = "2026-06-11T10:36:27.633Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/09/fe0e3bc32bd33707c519b102fc064ad2a2ce5a1b53e2be38b86936b476b1/cyclonedx_python_lib-11.7.0-py3-none-any.whl", hash = "sha256:02fa4f15ddbba21ac9093039f8137c0d1813af7fe88b760c5dcd3311a8da2178", size = 513041, upload-time = "2026-03-17T15:19:14.369Z" }, + { url = "https://files.pythonhosted.org/packages/a3/21/01c9b957ec3a778de86e010c1b54ea433ebf2fc50b810a3ca0ce8000782f/cyclonedx_python_lib-11.10.0-py3-none-any.whl", hash = "sha256:ffb9510b8d00a0896cfbe0a78b97c545d28f7d2a54e9d9dd5e5dc6e91ca9b375", size = 527798, upload-time = "2026-06-11T10:36:25.985Z" }, ] [[package]] @@ -928,11 +933,11 @@ wheels = [ [[package]] name = "distlib" -version = "0.4.0" +version = "0.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, ] [[package]] @@ -987,6 +992,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "falkordb" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/3a/58da510e5800a5cdbf9591111e4287cbf68b475bf074db12a94e5db8bcea/falkordb-1.6.1.tar.gz", hash = "sha256:bbef448a0b43e00ff3062bd6201368618d7b36e969d16ba71e8b8e3fa90873d4", size = 103185, upload-time = "2026-04-28T13:24:44.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/02/1cf9cef72228ca2b2776b4ed0cb2645298ddcc57c1fe2c545cd46bc11eae/falkordb-1.6.1-py3-none-any.whl", hash = "sha256:cf51caeb433c04db303de5967a0c2590675fcc0354e80997870b1e0497d30c34", size = 37802, upload-time = "2026-04-28T13:24:45.677Z" }, +] + [[package]] name = "faster-whisper" version = "1.2.1" @@ -1005,11 +1023,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.29.0" +version = "3.29.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/f5/3557bf28e0f1943e4849154c821533706e6dea010f96fb6aa0b6949037d1/filelock-3.29.3.tar.gz", hash = "sha256:7fc1b3f39cf172fd8203812043c57b8a65aef9969f38b6704f628b881f761a84", size = 61956, upload-time = "2026-06-10T17:37:11.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, + { url = "https://files.pythonhosted.org/packages/81/8f/b61d427c4f49a8bdadc93f4e7e74df8a6df6f77ee6e26bf0df53d3925363/filelock-3.29.3-py3-none-any.whl", hash = "sha256:e58333029cc9b925f39aad59b1d8f0a1ad836af4e60d7217f4a4dba87461261d", size = 42324, upload-time = "2026-06-10T17:37:10.37Z" }, ] [[package]] @@ -1131,7 +1149,7 @@ wheels = [ [[package]] name = "graphifyy" -version = "0.8.37" +version = "0.8.40" source = { editable = "." } dependencies = [ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1171,11 +1189,13 @@ dependencies = [ all = [ { name = "anthropic" }, { name = "boto3" }, + { name = "falkordb" }, { name = "faster-whisper", marker = "python_full_version >= '3.11'" }, { name = "graspologic", marker = "python_full_version < '3.13'" }, { name = "jieba" }, { name = "markdownify" }, - { name = "matplotlib" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "matplotlib", version = "3.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "mcp" }, { name = "neo4j" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, @@ -1202,6 +1222,9 @@ chinese = [ dm = [ { name = "tree-sitter-dm" }, ] +falkordb = [ + { name = "falkordb" }, +] gemini = [ { name = "openai" }, { name = "tiktoken" }, @@ -1219,9 +1242,17 @@ leiden = [ mcp = [ { name = "mcp" }, ] +minimax = [ + { name = "openai" }, + { name = "tiktoken" }, +] neo4j = [ { name = "neo4j" }, ] +nim = [ + { name = "openai" }, + { name = "tiktoken" }, +] office = [ { name = "openpyxl" }, { name = "python-docx" }, @@ -1244,7 +1275,8 @@ sql = [ { name = "tree-sitter-sql" }, ] svg = [ - { name = "matplotlib" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "matplotlib", version = "3.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] terraform = [ @@ -1284,6 +1316,8 @@ requires-dist = [ { name = "anthropic", marker = "extra == 'anthropic'" }, { name = "boto3", marker = "extra == 'all'" }, { name = "boto3", marker = "extra == 'bedrock'" }, + { name = "falkordb", marker = "extra == 'all'" }, + { name = "falkordb", marker = "extra == 'falkordb'" }, { name = "faster-whisper", marker = "python_full_version >= '3.11' and extra == 'all'" }, { name = "faster-whisper", marker = "python_full_version >= '3.11' and extra == 'video'" }, { name = "graspologic", marker = "python_full_version < '3.13' and extra == 'all'" }, @@ -1305,6 +1339,8 @@ requires-dist = [ { name = "openai", marker = "extra == 'all'" }, { name = "openai", marker = "extra == 'gemini'" }, { name = "openai", marker = "extra == 'kimi'" }, + { name = "openai", marker = "extra == 'minimax'" }, + { name = "openai", marker = "extra == 'nim'" }, { name = "openai", marker = "extra == 'ollama'" }, { name = "openai", marker = "extra == 'openai'" }, { name = "openpyxl", marker = "extra == 'all'" }, @@ -1319,45 +1355,47 @@ requires-dist = [ { name = "tiktoken", marker = "extra == 'all'" }, { name = "tiktoken", marker = "extra == 'gemini'" }, { name = "tiktoken", marker = "extra == 'kimi'" }, + { name = "tiktoken", marker = "extra == 'minimax'" }, + { name = "tiktoken", marker = "extra == 'nim'" }, { name = "tiktoken", marker = "extra == 'openai'" }, - { name = "tree-sitter", specifier = ">=0.23.0" }, - { name = "tree-sitter-bash" }, - { name = "tree-sitter-c" }, - { name = "tree-sitter-c-sharp" }, - { name = "tree-sitter-cpp" }, + { name = "tree-sitter", specifier = ">=0.23.0,<0.26" }, + { name = "tree-sitter-bash", specifier = ">=0.23,<0.27" }, + { name = "tree-sitter-c", specifier = ">=0.23,<0.25" }, + { name = "tree-sitter-c-sharp", specifier = ">=0.23,<0.25" }, + { name = "tree-sitter-cpp", specifier = ">=0.23,<0.25" }, { name = "tree-sitter-dm", marker = "extra == 'all'" }, { name = "tree-sitter-dm", marker = "extra == 'dm'" }, - { name = "tree-sitter-elixir" }, - { name = "tree-sitter-fortran" }, - { name = "tree-sitter-go" }, - { name = "tree-sitter-groovy" }, + { name = "tree-sitter-elixir", specifier = ">=0.3,<0.5" }, + { name = "tree-sitter-fortran", specifier = ">=0.6,<0.8" }, + { name = "tree-sitter-go", specifier = ">=0.23,<0.26" }, + { name = "tree-sitter-groovy", specifier = ">=0.1,<0.3" }, { name = "tree-sitter-hcl", marker = "extra == 'all'" }, { name = "tree-sitter-hcl", marker = "extra == 'terraform'" }, - { name = "tree-sitter-java" }, - { name = "tree-sitter-javascript" }, - { name = "tree-sitter-json" }, - { name = "tree-sitter-julia" }, - { name = "tree-sitter-kotlin" }, - { name = "tree-sitter-lua" }, - { name = "tree-sitter-objc" }, - { name = "tree-sitter-php" }, - { name = "tree-sitter-powershell" }, - { name = "tree-sitter-python" }, - { name = "tree-sitter-ruby" }, - { name = "tree-sitter-rust" }, - { name = "tree-sitter-scala" }, + { name = "tree-sitter-java", specifier = ">=0.23,<0.25" }, + { name = "tree-sitter-javascript", specifier = ">=0.23,<0.26" }, + { name = "tree-sitter-json", specifier = ">=0.23,<0.26" }, + { name = "tree-sitter-julia", specifier = ">=0.23,<0.25" }, + { name = "tree-sitter-kotlin", specifier = ">=1.0,<2.0" }, + { name = "tree-sitter-lua", specifier = ">=0.2,<0.6" }, + { name = "tree-sitter-objc", specifier = ">=3.0,<4.0" }, + { name = "tree-sitter-php", specifier = ">=0.23,<0.25" }, + { name = "tree-sitter-powershell", specifier = ">=0.26,<0.28" }, + { name = "tree-sitter-python", specifier = ">=0.23,<0.26" }, + { name = "tree-sitter-ruby", specifier = ">=0.23,<0.25" }, + { name = "tree-sitter-rust", specifier = ">=0.23,<0.25" }, + { name = "tree-sitter-scala", specifier = ">=0.23,<0.27" }, { name = "tree-sitter-sql", marker = "extra == 'all'" }, { name = "tree-sitter-sql", marker = "extra == 'sql'" }, - { name = "tree-sitter-swift" }, - { name = "tree-sitter-typescript" }, - { name = "tree-sitter-verilog" }, - { name = "tree-sitter-zig" }, + { name = "tree-sitter-swift", specifier = ">=0.7,<0.9" }, + { name = "tree-sitter-typescript", specifier = ">=0.23,<0.25" }, + { name = "tree-sitter-verilog", specifier = ">=1.0,<2.0" }, + { name = "tree-sitter-zig", specifier = ">=1.0,<2.0" }, { name = "watchdog", marker = "extra == 'all'" }, { name = "watchdog", marker = "extra == 'watch'" }, { name = "yt-dlp", marker = "extra == 'all'" }, { name = "yt-dlp", marker = "extra == 'video'" }, ] -provides-extras = ["mcp", "neo4j", "pdf", "watch", "svg", "leiden", "office", "google", "postgres", "video", "kimi", "ollama", "bedrock", "anthropic", "gemini", "openai", "chinese", "sql", "dm", "terraform", "all"] +provides-extras = ["mcp", "neo4j", "falkordb", "pdf", "watch", "svg", "leiden", "office", "google", "postgres", "video", "kimi", "ollama", "bedrock", "anthropic", "gemini", "minimax", "nim", "openai", "chinese", "sql", "dm", "terraform", "all"] [package.metadata.requires-dev] dev = [ @@ -1391,13 +1429,14 @@ dependencies = [ { name = "graspologic-native", marker = "python_full_version < '3.13'" }, { name = "hyppo", marker = "python_full_version < '3.13'" }, { name = "joblib", marker = "python_full_version < '3.13'" }, - { name = "matplotlib", marker = "python_full_version < '3.13'" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "matplotlib", version = "3.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "pot", marker = "python_full_version < '3.13'" }, { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "seaborn", marker = "python_full_version < '3.13'" }, @@ -1412,14 +1451,22 @@ wheels = [ [[package]] name = "graspologic-native" -version = "1.2.5" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/2d/62b30d89533643ccf4778a18eb023f291b8877b5d85de3342f07b2d363a7/graspologic_native-1.2.5.tar.gz", hash = "sha256:27ea7e01fa44466c0b4cdd678d4561e5d3dc0cb400015683b7ae1386031257a0", size = 2512729, upload-time = "2025-04-02T19:34:22.961Z" } +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/85/1729ca251be30ea6f137172ae8fd13c1300fdf15bb0e69be05f0260e28b0/graspologic_native-1.3.0.tar.gz", hash = "sha256:27301d885c0f47be0ed6496420de444a6f2c2032b0d19cfcc6869471ca654875", size = 2656005, upload-time = "2026-06-03T01:21:22.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/86/10748f4c474b0c8f6060dd379bb0c4da5d42779244bb13a58656ffb44a03/graspologic_native-1.2.5-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bf05f2e162ae2a2a8d6e8cfccbe3586d1faa0b808159ff950478348df557c61e", size = 648437, upload-time = "2025-04-02T19:34:16.29Z" }, - { url = "https://files.pythonhosted.org/packages/42/cc/b75ea35755340bedda29727e5388390c639ea533f55b9249f5ac3003f656/graspologic_native-1.2.5-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fff06ed49c3875cf351bb09a92ae7cbc169ce92dcc4c3439e28e801f822ae", size = 352044, upload-time = "2025-04-02T19:34:18.153Z" }, - { url = "https://files.pythonhosted.org/packages/8e/55/15e6e4f18bf249b529ac4cd1522b03f5c9ef9284a2f7bfaa1fd1f96464fe/graspologic_native-1.2.5-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e7e993e7d70fe0d860773fc62812fbb8cb4ef2d11d8661a1f06f8772593915", size = 364644, upload-time = "2025-04-02T19:34:19.486Z" }, - { url = "https://files.pythonhosted.org/packages/3b/51/21097af79f3d68626539ab829bdbf6cc42933f020e161972927d916e394c/graspologic_native-1.2.5-cp38-abi3-win_amd64.whl", hash = "sha256:c3ef2172d774083d7e2c8e77daccd218571ddeebeb2c1703cebb1a2cc4c56e07", size = 210438, upload-time = "2025-04-02T19:34:21.139Z" }, + { url = "https://files.pythonhosted.org/packages/16/99/32b39ecdcaaab1daf5756f9e84a83b768673f56d75edeb76364ed9d5da3f/graspologic_native-1.3.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:db2ede7b6095a62b9e0dc28185436954db3dd90e6fe2dbc31cd9324097b12c68", size = 728135, upload-time = "2026-06-03T01:21:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/89/59/af503e7f1c7e4801a9902892e3b9c197adf59a0b2e4ca18df8fb0ec8e93d/graspologic_native-1.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ccfc98653686a8fdad42ad7d8ccc522e807d92966e4ecc437d9ff87c260a3bd", size = 400398, upload-time = "2026-06-03T01:21:13.867Z" }, + { url = "https://files.pythonhosted.org/packages/f5/fe/8d46d0503717c192a158b70ef10f211715a5b15d4caa79e53d5bc52c8c03/graspologic_native-1.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c9f6a1e5698893cecd284b9ef07024493c5e414f1efcf17fc1c063e23b47660", size = 416514, upload-time = "2026-06-03T01:21:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0e/bfb2fe307b0428822215ca91d02c0d7426f3c6a0597b5b7e10690ded66c8/graspologic_native-1.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e519ed7dc6a836d523947be84d3b573e1f99cd80c1bec15a6265774299b0667e", size = 468501, upload-time = "2026-06-03T01:21:16.721Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b0/fbb8a424e227b346174f0bc68f4a3cd68f0adec200c5de0ee8bf6304eba7/graspologic_native-1.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e711fd2817cdaea4d2dcb37cd2acb3b18bb7d7860bed4f1d7e64f138cdf1a7ed", size = 617479, upload-time = "2026-06-03T01:21:18.152Z" }, + { url = "https://files.pythonhosted.org/packages/7f/51/de9902b258802de8c227baa014f9cb1c9793872f24f5ba8a78117f030b21/graspologic_native-1.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:3b0cc5102c4da829097f651b351acc45c4dfe09301abedfde97803f3f6e5aafc", size = 260654, upload-time = "2026-06-03T01:21:19.388Z" }, + { url = "https://files.pythonhosted.org/packages/0a/76/2072fed5a5b9d4660358cd453ba43bba8486c1b5e46cb76c955db373c058/graspologic_native-1.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:41e0df69a51c3feb27b15826fa22befd768f20bf33cfe99b55390162ab644a66", size = 245006, upload-time = "2026-06-03T01:21:20.622Z" }, ] [[package]] @@ -1433,34 +1480,34 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, - { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, - { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, - { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, - { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, - { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, - { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, - { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, - { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, - { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, - { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, - { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, - { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, - { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, - { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, - { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/ee/dd9ba7beae1005e54131b7d45263cc74c8a066d47d354e6d58ae9445a388/hf_xet-1.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:dbf48c0d02cf0b2e568944330c60d9120c272dabe013bd892d48e25bc6797577", size = 4069485, upload-time = "2026-06-08T23:02:13.193Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/9cae6cfeb4e03070874e73e5c97c66eb90369d3206b6a2b1ef5f96520888/hf_xet-1.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78e4e5192ad2b674c2e1160b651cb9134db974f8ae1835bdfbfb0166b894a43", size = 3838493, upload-time = "2026-06-08T23:02:15.282Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b4/d5c01e0eb6d9f2ca2dacd84d0d1b71e6cfbb2ef3208c968528e010e9b3d7/hf_xet-1.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f7a04a8ad962422e225bc49fbbac99dc1806764b1f3e54dbd154bffa7593947", size = 4505658, upload-time = "2026-06-08T23:02:17.196Z" }, + { url = "https://files.pythonhosted.org/packages/76/c5/29a7598c0c6383c523dc22186d577f4e04267a626cd95ae60f67c00bfe66/hf_xet-1.5.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d48199c2bf4f8df0adc55d31d1368b6ec0e4d4f45bc86b08038089c23db0bed8", size = 4292822, upload-time = "2026-06-08T23:02:18.608Z" }, + { url = "https://files.pythonhosted.org/packages/04/9a/dceaf6ca69390126b86ea825fb354b93d01163199070b7bd849225de9468/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:97f212a88d14bbf573619a74b7fecb238de77d08fc702e54dec6f78276ca3283", size = 4491255, upload-time = "2026-06-08T23:02:20.124Z" }, + { url = "https://files.pythonhosted.org/packages/48/a7/e5a7afaacf6c1791fdbeeac42951fb81c3d2bc482992b115dedcc86d963e/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f61e3665892a6c8c5e765395838b8ddf36185da835253d4bc4509a81e49fb342", size = 4711062, upload-time = "2026-06-08T23:02:21.863Z" }, + { url = "https://files.pythonhosted.org/packages/53/49/2802f8433c9742ce281bddc1e65c02c32268ca3098d66828b05e12e45ee2/hf_xet-1.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f4ad3ebd4c32dd2b27099d69dc7b2df821e30767e46fb6ee6a0713778243b8ff", size = 4017205, upload-time = "2026-06-08T23:02:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5a/50c71195b9fb883659f596e7252faf4c18c58e753a9013bdbf9bac5d2250/hf_xet-1.5.1-cp313-cp313t-win_arm64.whl", hash = "sha256:8298485c1e36e7e67cbd01eeb1376619b7af43d4f1ec245caae306f890a8a32d", size = 3845426, upload-time = "2026-06-08T23:02:25.124Z" }, + { url = "https://files.pythonhosted.org/packages/05/24/5e0c28f80371c17d49fed004597d9d132cb75c1f6f53db2cb95f459d2312/hf_xet-1.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3474760d10e3bb6f92ff3f024fcb00c0b3e4001e9b035c7483e49a5dd17aa70f", size = 4069676, upload-time = "2026-06-08T23:02:26.759Z" }, + { url = "https://files.pythonhosted.org/packages/d2/17/261ba565b6a4d960fb478f61fdf919c0be5824645aaf1c319eca660c1611/hf_xet-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6762d89b9e3267dfd502b29b2a327b4525f33b17e7b509a78d94e2151a30ce30", size = 3838509, upload-time = "2026-06-08T23:02:28.573Z" }, + { url = "https://files.pythonhosted.org/packages/4e/44/7ffdc2e184b0d41fc0f683ba3936ef669ab63cf242cf36ef50e57d683668/hf_xet-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf67e6ed10260cef62e852789dc91ebb03f382d5bdc4b1dbeb64763ea275e7d6", size = 4505881, upload-time = "2026-06-08T23:02:30.257Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/788060d5aa4d5e671f1a31bf69624c314eb2d8babab3aa562f9e5d53444e/hf_xet-1.5.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c6b6cd08ca095058780b50b8ce4d6cbf6787bcf27841705d58a9d32246e3e47a", size = 4292995, upload-time = "2026-06-08T23:02:31.993Z" }, + { url = "https://files.pythonhosted.org/packages/22/93/c5540cbd6b55529b7dc42f6734e88cebee21aefbea34128b66229df56c57/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1af0de8ca6f190d4294a28b88023db64a1e2d1d719cab044baf75bec569e7a9", size = 4491570, upload-time = "2026-06-08T23:02:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/03/f3/9d8ceab30f44f36c1679b1b8683054c71a0dadc787dbf07421891742d3ca/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4f561cbbb92f80960772059864b7fb07eae879adde1b2e781ec6f86f6ac26c59", size = 4711565, upload-time = "2026-06-08T23:02:35.454Z" }, + { url = "https://files.pythonhosted.org/packages/cd/54/27ed9a5e2cc583b4df82f75a03a4df8dbf55f5a9fa1f47f1fadfb20dbeac/hf_xet-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e7dbb40617410f432182d918e37c12303fe6700fd6aa6c5964e30a535a4461d6", size = 4017343, upload-time = "2026-06-08T23:02:37.14Z" }, + { url = "https://files.pythonhosted.org/packages/ae/12/ecb2fc8d45e767580e3a37faa97cb895608b614965567efb4f18cff67e27/hf_xet-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6071d5ccb4d8d2cbd5fea5cc798da4f0ba3f44e25369591c4e89a4987050e61d", size = 3845716, upload-time = "2026-06-08T23:02:39.073Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" }, ] [[package]] @@ -1502,9 +1549,10 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.15.0" +version = "1.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "click", marker = "python_full_version >= '3.11'" }, { name = "filelock", marker = "python_full_version >= '3.11'" }, { name = "fsspec", marker = "python_full_version >= '3.11'" }, { name = "hf-xet", marker = "(python_full_version >= '3.11' and platform_machine == 'AMD64') or (python_full_version >= '3.11' and platform_machine == 'aarch64') or (python_full_version >= '3.11' and platform_machine == 'amd64') or (python_full_version >= '3.11' and platform_machine == 'arm64') or (python_full_version >= '3.11' and platform_machine == 'x86_64')" }, @@ -1515,22 +1563,22 @@ dependencies = [ { name = "typer", marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/b6/e22bd20a25299c34b8c5922c1545a6320825b13906eb0f7298edfd034a0b/huggingface_hub-1.15.0.tar.gz", hash = "sha256:28abfdddda3927fd4de6a63cf26ab012498a2c24dae52baf150c5c6edf98a1d5", size = 784100, upload-time = "2026-05-15T11:42:52.149Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/27/629cfe58c582f92ded066c4a07d1a057ff617118ab7973200f770bd853cb/huggingface_hub-1.19.0.tar.gz", hash = "sha256:fd771622182d40977272a923953ee3b1b13538f9f8a7f5d78398f10af0f1c0bd", size = 824721, upload-time = "2026-06-11T12:33:18.665Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/11/0b64cc9024329b76d7547c19a67604a61d21d3ba678a69d1b220c29d5112/huggingface_hub-1.15.0-py3-none-any.whl", hash = "sha256:a4a59af04cbc41a3fe3fec429b171ef994ef8c971eda10136746f408dd4e3744", size = 663602, upload-time = "2026-05-15T11:42:50.487Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a5/558da89f66464d8d0229ff497e8b8666977de2d8cf48c28a2862ecf1250f/huggingface_hub-1.19.0-py3-none-any.whl", hash = "sha256:1dc72e1f6b4d6df6b30eb72e57d00514ef453d660f04af2b87f0e67267f31ee0", size = 693398, upload-time = "2026-06-11T12:33:16.695Z" }, ] [[package]] name = "hypothesis" -version = "6.153.0" +version = "6.155.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/92/918fb03318c7ff9a271d7cad8eceb359d1069f17e84f5191d52c2970f18f/hypothesis-6.153.0.tar.gz", hash = "sha256:11616e5158fc485d62bae19d9cc69333237faa8050ad44a45218254a1ef272bb", size = 474030, upload-time = "2026-05-26T05:19:05.468Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/04/64032a1dccd2233615c8a3f701bbb563558575ed017496a24b6d81762c91/hypothesis-6.155.2.tar.gz", hash = "sha256:ae36880287c9c5defe9f199d3d2b67d9947a4da2a46e6c57373cbdf2345b20e1", size = 477765, upload-time = "2026-06-05T16:32:23.63Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/20/96dc2387cf29a0ec75b427d62d3dde1f44c924719503babaac4c96806223/hypothesis-6.153.0-py3-none-any.whl", hash = "sha256:2aeda9bbb44ae0ee0bfa67ef744a25be05c1f804dca4eb6479c63518dc9f2900", size = 540326, upload-time = "2026-05-26T05:19:02.861Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6e/e735f27ac1a530a4cd0a31cd970ec495a3a11830fdc5d281cc292593b330/hypothesis-6.155.2-py3-none-any.whl", hash = "sha256:c85ce6dcd630a90ce501f1d1dd1bc84b97f5649ca8a27e134c8cbf5aa480b1a5", size = 544213, upload-time = "2026-06-05T16:32:21.15Z" }, ] [[package]] @@ -1546,7 +1594,7 @@ dependencies = [ { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "patsy", marker = "python_full_version < '3.13'" }, { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "statsmodels", marker = "python_full_version < '3.13'" }, @@ -1567,11 +1615,11 @@ wheels = [ [[package]] name = "idna" -version = "3.15" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -1615,105 +1663,105 @@ wheels = [ [[package]] name = "jiter" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/2e/a9959997739c403378d0a4a3a1c4ed80b60aeace216c4d37b303a9fc60a4/jiter-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:02f36a5c700f105ac04a6556fe664a59037a2c200db3b7e88784fac2ddf02531", size = 316927, upload-time = "2026-04-10T14:25:40.753Z" }, - { url = "https://files.pythonhosted.org/packages/27/72/b6de8a531e0adbadd839bec301165feb1fccf00e9ff55073ba2dd20f0043/jiter-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41eab6c09ceffb6f0fe25e214b3068146edb1eda3649ca2aee2a061029c7ba2e", size = 321181, upload-time = "2026-04-10T14:25:42.621Z" }, - { url = "https://files.pythonhosted.org/packages/db/d8/2040b9efa13c917f855c40890ae4119fe02c25b7c7677d5b4fa820a851fc/jiter-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf4d4c109641f9cfaf4a7b6aebd51654e405cd00fa9ebbf87163b8b97b325aa", size = 347387, upload-time = "2026-04-10T14:25:44.212Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/655c0ad5ce6a8e90f9068c175b8a236877d753e460762b3183c136db1c5b/jiter-0.14.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80c7b41a628e6be2213ad0ece763c5f88aa5ee003fa394d58acaaee1f4b8342", size = 373083, upload-time = "2026-04-10T14:25:45.55Z" }, - { url = "https://files.pythonhosted.org/packages/f1/66/549c40fa068f08710b7570869c306a051eb67a29758bd64f4114f730554c/jiter-0.14.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb3dbf7cc0d4dbe73cce307ebe7eefa7f73a7d3d854dd119ea0c243f03e40927", size = 463639, upload-time = "2026-04-10T14:25:47.452Z" }, - { url = "https://files.pythonhosted.org/packages/25/2f/97a32a05fed14ed58a18e181fdfb619e05163f3726b54ee6080ec0539c09/jiter-0.14.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7054adcdeb06b46efd17b5734f75817a44a2d06d3748e36c3a023a1bb52af9ec", size = 380735, upload-time = "2026-04-10T14:25:49.305Z" }, - { url = "https://files.pythonhosted.org/packages/2a/3b/4347e1d6c2a973d653bbb7a2d671a2d2426e54b52ba735b8ff0d0a29b75c/jiter-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d597cd1bf6790376f3fffc7c708766e57301d99a19314824ea0ccc9c3c70e1e2", size = 358632, upload-time = "2026-04-10T14:25:50.931Z" }, - { url = "https://files.pythonhosted.org/packages/ef/24/ca452fbf2ea33548ed30ce68a39a50442d3f7c9bf0704a7af958a930c057/jiter-0.14.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:df63a14878da754427926281626fd3ee249424a186e25a274e78176d42945264", size = 359969, upload-time = "2026-04-10T14:25:52.381Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a3/94470a0d199287caabeb4da2bb2ae5f6d17f3cf05dfc975d7cb064d58e0f/jiter-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ea73187627bcc5810e085df715e8a99da8bdfd96a7eb36b4b4df700ba6d4c9c", size = 397529, upload-time = "2026-04-10T14:25:53.801Z" }, - { url = "https://files.pythonhosted.org/packages/cf/71/6768edc09d7c45c39f093feb3de105fa718a3e982b5208b8a2ed6382b44b/jiter-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9f541eaf7bb8382367a1a23d6fc3d6aad57f8dd8c18c3c17f838bee20f217220", size = 522342, upload-time = "2026-04-10T14:25:55.396Z" }, - { url = "https://files.pythonhosted.org/packages/3d/6b/5c2e17559a0f4e96e934479f7137df46c939e983fa05244e674815befb73/jiter-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:107465250de4fce00fdb47166bcd51df8e634e049541174fe3c71848e44f52ce", size = 556784, upload-time = "2026-04-10T14:25:56.927Z" }, - { url = "https://files.pythonhosted.org/packages/b1/83/c25f3556a60fc74d11199100f1b6cc0c006b815c8494dea8ca16fe398732/jiter-0.14.0-cp310-cp310-win32.whl", hash = "sha256:ffb2a08a406465bb076b7cc1df41d833106d3cf7905076cc73f0cb90078c7d10", size = 208439, upload-time = "2026-04-10T14:25:58.796Z" }, - { url = "https://files.pythonhosted.org/packages/2e/99/781a1b413f0989b7f2ea203b094b331685f1a35e52e0a45e5d000ecaab27/jiter-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb8b682d10cb0cce7ff4c1af7244af7022c9b01ae16d46c357bdd0df13afb25d", size = 204558, upload-time = "2026-04-10T14:26:00.208Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/198ae537fccb7080a0ed655eb56abf64a92f79489dfbf79f40fa34225bcd/jiter-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e791e247b8044512e070bd1f3633dc08350d32776d2d6e7473309d0edf256a2", size = 316896, upload-time = "2026-04-10T14:26:01.986Z" }, - { url = "https://files.pythonhosted.org/packages/cf/34/da67cff3fce964a36d03c3e365fb0f8726ade2a6cfd4d3c70107e216ead6/jiter-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71527ce13fd5a0c4e40ad37331f8c547177dbb2dd0a93e5278b6a5eecf748804", size = 321085, upload-time = "2026-04-10T14:26:03.364Z" }, - { url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" }, - { url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" }, - { url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" }, - { url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" }, - { url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" }, - { url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/bbb2189f62ace8d95e869aa4c84c9946616f301e2d02895a6f20dcc3bba3/jiter-0.14.0-cp311-cp311-win32.whl", hash = "sha256:4927d09b3e572787cc5e0a5318601448e1ab9391bcef95677f5840c2d00eaa6d", size = 208660, upload-time = "2026-04-10T14:26:20.511Z" }, - { url = "https://files.pythonhosted.org/packages/b8/86/c500b53dcbf08575f5963e536ebd757a1f7c568272ba5d180b212c9a87fb/jiter-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:42d6ed359ac49eb922fdd565f209c57340aa06d589c84c8413e42a0f9ae1b842", size = 204659, upload-time = "2026-04-10T14:26:22.152Z" }, - { url = "https://files.pythonhosted.org/packages/75/4a/a676249049d42cb29bef82233e4fe0524d414cbe3606c7a4b311193c2f77/jiter-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:6dd689f5f4a5a33747b28686e051095beb214fe28cfda5e9fe58a295a788f593", size = 194772, upload-time = "2026-04-10T14:26:23.458Z" }, - { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, - { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, - { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, - { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, - { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, - { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, - { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, - { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, - { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, - { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, - { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, - { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, - { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, - { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, - { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, - { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, - { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, - { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, - { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, - { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, - { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, - { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, - { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, - { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, - { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, - { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, - { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, - { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, - { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, - { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, - { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, - { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, - { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, - { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, - { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, - { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, - { url = "https://files.pythonhosted.org/packages/32/a1/ef34ca2cab2962598591636a1804b93645821201cc0095d4a93a9a329c9d/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a25ffa2dbbdf8721855612f6dca15c108224b12d0c4024d0ac3d7902132b4211", size = 311366, upload-time = "2026-04-10T14:28:27.943Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/520576a532a6b8a6f42747afed289c8448c879a34d7802fe2c832d4fd38f/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ac9cbaa86c10996b92bd12c91659b60f939f8e28fcfa6bc11a0e90a774ce95b", size = 309873, upload-time = "2026-04-10T14:28:29.688Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" }, - { url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" }, - { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, - { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, - { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/da/76a2c7e510ba15fe323d9509c223ab272da79ea59f54488f4a78da6426db/jiter-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:edebcf7d1f601199084bb6e844d7dc67e03e04f6ac786b0332d616635c4ff7a4", size = 310849, upload-time = "2026-05-19T10:06:51.944Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8e/827be942883a4dc0862c48626ff41af3320b1902d136a0bf4b9041f2c567/jiter-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f924585cdacf631cd382b657966847bb537bf9ed0a6f9b991da5f05a631480f", size = 314991, upload-time = "2026-05-19T10:06:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/6d/38/be2832be361ba1b9517c76f46d30b64e985be1dd43c974f4c3a4b1844436/jiter-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abbf258599526ad0326fe51e252e24f2bd6f24f1852681b4b78feda3808f1d18", size = 340843, upload-time = "2026-05-19T10:06:55.071Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/90f01fb83c0c7ba509303ec93e32a308fbfa167d264860b01c0fd0dbbd06/jiter-0.15.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c468136b8bd6bb18c8786e4236a1fa27362f24cb23450ba0cb204ab379b8e6f", size = 365116, upload-time = "2026-05-19T10:06:56.893Z" }, + { url = "https://files.pythonhosted.org/packages/91/38/94593d34f8c67a0b6f6cbc027f016ffa9780b3a858a7a86f6fd7a15bcc1e/jiter-0.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05906b93d72f03339e6bb7cf8dc10ebda64a0266126eed6beba79e20abcf5fd4", size = 457970, upload-time = "2026-05-19T10:06:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/df/04/d79962dd49d00c97e2a9b4cacea1947904d02135936960351f9a96d4c1a6/jiter-0.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30ce785d2adb8e32c3f7741442370a74834ec4c01f3c48f0750227a0b4ef27d6", size = 375744, upload-time = "2026-05-19T10:07:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2e/5d37abe2be0e819c21e2338bebd410e481763ce526a9138c8c3652fa0123/jiter-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd73e3da91a0a722d67165e849ce2cdc10de0e0d48738c142be8c6c5f310f4c", size = 349609, upload-time = "2026-05-19T10:07:01.829Z" }, + { url = "https://files.pythonhosted.org/packages/7a/90/98768ad2ed90c1fda15d64157de2dfbf73c1c074d4b1bfaca915480bc7cf/jiter-0.15.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:ceb8fc27d38793f9c97149be8302720c5b22e5c195a37bf2c45dc36c4600a512", size = 354366, upload-time = "2026-05-19T10:07:03.587Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c4/fbfb806209f1fe4b7dccdfb07bc62bb044300734a945b06fd64db446ef6a/jiter-0.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d726e3ceeb337191324b49de298142f27c3ad10886341555d1d5315b5f252c6a", size = 393519, upload-time = "2026-05-19T10:07:05.08Z" }, + { url = "https://files.pythonhosted.org/packages/37/1c/b9c257cd70cb453b6d10f3ebf0402cdb11669ab455389096f09839670290/jiter-0.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c8aea7781d2a372227871de4e1a1332aa96f5a89fd76c5e835dafdbad102887", size = 519952, upload-time = "2026-05-19T10:07:06.589Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1a/aa85027db7ab15829c12feebbc33b404f53fc399bd559d85fd0d6365ff0d/jiter-0.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf4bd113a69c0a740e27cb962ce10630c36d2b8f59d759a651b955ee9d18a823", size = 550770, upload-time = "2026-05-19T10:07:08.228Z" }, + { url = "https://files.pythonhosted.org/packages/d4/54/8c3f65c8a5687925e84708f19d63f7f37d28e2b86a48d951702ad94424d8/jiter-0.15.0-cp310-cp310-win32.whl", hash = "sha256:d92a5cd21fdb083931d546c207aa29633787c5dc5b02daab2d32b843f88a2c53", size = 209303, upload-time = "2026-05-19T10:07:10.006Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/0528a1eb9f42dd2d8228a0711458628f35924d131f623eaebc35fd23d3d4/jiter-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:e58585a58209d72691ce2d62a9147445f5a87beb0bde97fde284c96ae392a3d1", size = 200404, upload-time = "2026-05-19T10:07:11.426Z" }, + { url = "https://files.pythonhosted.org/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2", size = 310899, upload-time = "2026-05-19T10:07:12.89Z" }, + { url = "https://files.pythonhosted.org/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67", size = 314963, upload-time = "2026-05-19T10:07:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a", size = 341730, upload-time = "2026-05-19T10:07:15.869Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7", size = 366214, upload-time = "2026-05-19T10:07:17.259Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd", size = 459527, upload-time = "2026-05-19T10:07:18.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281", size = 375451, upload-time = "2026-05-19T10:07:20.208Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708", size = 349428, upload-time = "2026-05-19T10:07:22.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928", size = 355405, upload-time = "2026-05-19T10:07:23.916Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd", size = 393688, upload-time = "2026-05-19T10:07:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e", size = 520853, upload-time = "2026-05-19T10:07:27.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef", size = 551016, upload-time = "2026-05-19T10:07:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32", size = 209518, upload-time = "2026-05-19T10:07:30.431Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04", size = 200565, upload-time = "2026-05-19T10:07:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865", size = 195488, upload-time = "2026-05-19T10:07:33.846Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, + { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, + { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" }, + { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" }, + { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" }, + { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" }, + { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" }, + { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750", size = 305585, upload-time = "2026-05-19T10:09:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b", size = 306936, upload-time = "2026-05-19T10:09:37.435Z" }, + { url = "https://files.pythonhosted.org/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b", size = 342453, upload-time = "2026-05-19T10:09:39.294Z" }, + { url = "https://files.pythonhosted.org/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c", size = 345606, upload-time = "2026-05-19T10:09:40.727Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, ] [[package]] @@ -1736,14 +1784,14 @@ wheels = [ [[package]] name = "joserfc" -version = "1.6.7" +version = "1.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/cb/52e479f20804904f5df20ac4539d292dcecd1287aaa33cba1d1def1d9d8e/joserfc-1.6.7.tar.gz", hash = "sha256:6999fe89457069ecacd8cc797c88a805f83054dd883333fa0409f74b46479fd7", size = 232158, upload-time = "2026-05-23T01:46:44.069Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/90/25cb27518750218e4f850be63d8bbb2343efaad1c01c3571aaa4b3c33bd7/joserfc-1.7.1.tar.gz", hash = "sha256:77d0b76514879c68c6f433bc5b7357a4ab72008ff1e33d8379fd11d72bd8ca81", size = 233181, upload-time = "2026-06-08T07:21:33.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/e4/bcf6718b5662894c6831f46296b73cd4b1a2e90c20b6d437e20c4997388c/joserfc-1.6.7-py3-none-any.whl", hash = "sha256:9e51e4a64840aa1734a058258e80a4480e2ff2d5686e480e7c92c954a92fbe05", size = 70603, upload-time = "2026-05-23T01:46:42.129Z" }, + { url = "https://files.pythonhosted.org/packages/b3/00/fa62404c3e347f946faa13aa21085205f9cc06ad17671e37f81a51662ae8/joserfc-1.7.1-py3-none-any.whl", hash = "sha256:b3e3d655612e2e1ef67b2600f2f420e12e537b020208fab1761fad647319c164", size = 70423, upload-time = "2026-06-08T07:21:32.001Z" }, ] [[package]] @@ -1754,7 +1802,8 @@ dependencies = [ { name = "attrs" }, { name = "jsonschema-specifications" }, { name = "referencing" }, - { name = "rpds-py" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ @@ -1943,120 +1992,120 @@ wheels = [ [[package]] name = "lxml" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/6e/ee8fc0e01202eb3dd2b9e1ea4f0910d72425d35c66187c63931d7a3ea73f/lxml-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec", size = 8540733, upload-time = "2026-04-18T04:27:33.185Z" }, - { url = "https://files.pythonhosted.org/packages/54/e8/325fe9b942824c773dffe1baf0c35b046a763851fdff4393af4450bceeb7/lxml-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec", size = 4602805, upload-time = "2026-04-18T04:27:36.097Z" }, - { url = "https://files.pythonhosted.org/packages/2d/81/221aa3ea4a40370bb0358fa454cbe7e5a837e522f7630c24dfef3f9a73b0/lxml-6.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1bc4cc83fb7f66ffb16f74d6dd0162e144333fc36ebcce32246f80c8735b2551", size = 5002652, upload-time = "2026-04-18T04:27:30.603Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e1/fdbfb9019542f1875c093576df7f37adc2983c8ba7ecf17e5f14490bc107/lxml-6.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20cf4d0651987c906a2f5cba4e3a8d6ba4bfdf973cfe2a96c0d6053888ea2ecd", size = 5155332, upload-time = "2026-04-18T04:27:33.507Z" }, - { url = "https://files.pythonhosted.org/packages/56/b1/4087c782fff397cd03abf9c551069be59bb04a7e548c50fb7b9c4cdaca28/lxml-6.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb34ea45a82dd637c2c97ae1bbb920850c1e59bcae79ce1c15af531d83e7215", size = 5057226, upload-time = "2026-04-18T04:27:37.567Z" }, - { url = "https://files.pythonhosted.org/packages/5d/66/516c79dec8417f3a972327330254c0b5fac93d5c3ecfd8a5b43650a5a4d9/lxml-6.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d9b99e5b2597e4f5aed2484fef835256fa1b68a19e4265c97628ef4bf8bcf4", size = 5287588, upload-time = "2026-04-18T04:27:41.4Z" }, - { url = "https://files.pythonhosted.org/packages/94/1d/e578f4cbeb42b9df9f29b0d44a45a7cdfa3a5ae300dd59ec68e3602d29bb/lxml-6.1.0-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:d43aa26dcda363f21e79afa0668f5029ed7394b3bb8c92a6927a3d34e8b610ea", size = 5412438, upload-time = "2026-04-18T04:27:45.589Z" }, - { url = "https://files.pythonhosted.org/packages/47/5b/2aa68307d6d15959e84d4882f9c04f2da63127eac463e1594166f681ef77/lxml-6.1.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:6262b87f9e5c1e5fe501d6c153247289af42eb44ad7660b9b3de17baaf92d6f6", size = 4770997, upload-time = "2026-04-18T04:27:49.853Z" }, - { url = "https://files.pythonhosted.org/packages/ae/c9/3e51fc1228310a836b4eb32595ae00154ab12197fca944676a3ab3b163ea/lxml-6.1.0-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1392c569c032f78a11a25d1de1c43fff13294c793b39e19d84fade3045cbbc3", size = 5359678, upload-time = "2026-04-18T04:31:56.184Z" }, - { url = "https://files.pythonhosted.org/packages/b5/91/ab8bc834f977fbbd310e697b120787c153db026f9151e02a88d2645d4e5b/lxml-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:045e387d1f4f42a418380930fa3f45c73c9b392faf67e495e58902e68e8f44a7", size = 5107890, upload-time = "2026-04-18T04:32:00.387Z" }, - { url = "https://files.pythonhosted.org/packages/bb/10/8a143cfa3ac99cb5b0523ff6d0429a9c9dddf25ffeae09caa3866c7964d9/lxml-6.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9f93d5b8b07f73e8c77e3c6556a3db269918390c804b5e5fcdd4858232cc8f16", size = 4803977, upload-time = "2026-04-18T04:32:05.099Z" }, - { url = "https://files.pythonhosted.org/packages/45/fd/ee02faf52fa39c2fe32f824628958b9aa86dff21343dc3161f0e3c6ccd15/lxml-6.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:de550d129f18d8ab819651ffe4f38b1b713c7e116707de3c0c6400d0ef34fbc1", size = 5350277, upload-time = "2026-04-18T04:32:09.176Z" }, - { url = "https://files.pythonhosted.org/packages/85/8c/b3481364b8554b5d36d540189a87fc71e94b0b01c24f8f152bd662dd2e45/lxml-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c08da09dc003c9e8c70e06b53a11db6fb3b250c21c4236b03c7d7b443c318e7a", size = 5309717, upload-time = "2026-04-18T04:32:13.303Z" }, - { url = "https://files.pythonhosted.org/packages/74/e8/a6b21927077a9127afa17473b6576b322616f34ac50ee4f577e763b75ec0/lxml-6.1.0-cp310-cp310-win32.whl", hash = "sha256:37448bf9c7d7adfc5254763901e2bbd6bb876228dfc1fc7f66e58c06368a7544", size = 3598491, upload-time = "2026-04-18T04:27:24.288Z" }, - { url = "https://files.pythonhosted.org/packages/ea/82/14dea800d041274d96c07d49ff9191f011d1427450850de19bf541e2cc12/lxml-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:2593a0a6621545b9095b71ad74ed4226eba438a7d9fc3712a99bdb15508cf93a", size = 4020906, upload-time = "2026-04-18T04:27:27.53Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ba/d3539aaf4d9d21456b9a7b902816623227d05d63e7c5aafd8834c4b9bed6/lxml-6.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80807d72f96b96ad5588cb85c75616e4f2795a7737d4630784c51497beb7776", size = 3667787, upload-time = "2026-04-18T04:27:29.407Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5d/3bccad330292946f97962df9d5f2d3ae129cce6e212732a781e856b91e07/lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9", size = 8526232, upload-time = "2026-04-18T04:27:40.389Z" }, - { url = "https://files.pythonhosted.org/packages/a7/51/adc8826570a112f83bb4ddb3a2ab510bbc2ccd62c1b9fe1f34fae2d90b57/lxml-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50", size = 4595448, upload-time = "2026-04-18T04:27:44.208Z" }, - { url = "https://files.pythonhosted.org/packages/54/84/5a9ec07cbe1d2334a6465f863b949a520d2699a755738986dcd3b6b89e3f/lxml-6.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5", size = 4923771, upload-time = "2026-04-18T04:32:17.402Z" }, - { url = "https://files.pythonhosted.org/packages/a7/23/851cfa33b6b38adb628e45ad51fb27105fa34b2b3ba9d1d4aa7a9428dfe0/lxml-6.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e", size = 5068101, upload-time = "2026-04-18T04:32:21.437Z" }, - { url = "https://files.pythonhosted.org/packages/b0/38/41bf99c2023c6b79916ba057d83e9db21d642f473cac210201222882d38b/lxml-6.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512", size = 5002573, upload-time = "2026-04-18T04:32:25.373Z" }, - { url = "https://files.pythonhosted.org/packages/c2/20/053aa10bdc39747e1e923ce2d45413075e84f70a136045bb09e5eaca41d3/lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c", size = 5202816, upload-time = "2026-04-18T04:32:29.393Z" }, - { url = "https://files.pythonhosted.org/packages/9a/da/bc710fad8bf04b93baee752c192eaa2210cd3a84f969d0be7830fea55802/lxml-6.1.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5", size = 5329999, upload-time = "2026-04-18T04:32:34.019Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cb/bf035dedbdf7fab49411aa52e4236f3445e98d38647d85419e6c0d2806b9/lxml-6.1.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289", size = 4659643, upload-time = "2026-04-18T04:32:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4f/22be31f33727a5e4c7b01b0a874503026e50329b259d3587e0b923cf964b/lxml-6.1.0-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a", size = 5265963, upload-time = "2026-04-18T04:32:41.881Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2b/d44d0e5c79226017f4ab8c87a802ebe4f89f97e6585a8e4166dffcdd7b6e/lxml-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3", size = 5045444, upload-time = "2026-04-18T04:32:44.512Z" }, - { url = "https://files.pythonhosted.org/packages/d3/c3/3f034fec1594c331a6dbf9491238fdcc9d66f68cc529e109ec75b97197e1/lxml-6.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9", size = 4712703, upload-time = "2026-04-18T04:32:47.16Z" }, - { url = "https://files.pythonhosted.org/packages/12/16/0b83fccc158218aca75a7aa33e97441df737950734246b9fffa39301603d/lxml-6.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11", size = 5252745, upload-time = "2026-04-18T04:32:50.427Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ee/12e6c1b39a77666c02eaa77f94a870aaf63c4ac3a497b2d52319448b01c6/lxml-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4", size = 5226822, upload-time = "2026-04-18T04:32:53.437Z" }, - { url = "https://files.pythonhosted.org/packages/34/20/c7852904858b4723af01d2fc14b5d38ff57cb92f01934a127ebd9a9e51aa/lxml-6.1.0-cp311-cp311-win32.whl", hash = "sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3", size = 3594026, upload-time = "2026-04-18T04:27:31.903Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7", size = 4025114, upload-time = "2026-04-18T04:27:34.077Z" }, - { url = "https://files.pythonhosted.org/packages/c2/df/c84dcc175fd690823436d15b41cb920cd5ba5e14cd8bfb00949d5903b320/lxml-6.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39", size = 3667742, upload-time = "2026-04-18T04:27:38.45Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a4/053745ce1f8303ccbb788b86c0db3a91b973675cefc42566a188637b7c40/lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93", size = 4624024, upload-time = "2026-04-18T04:27:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" }, - { url = "https://files.pythonhosted.org/packages/94/7c/e08a970727d556caa040a44773c7b7e3ad0f0d73dedc863543e9a8b931f2/lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a", size = 5093820, upload-time = "2026-04-18T04:32:58.94Z" }, - { url = "https://files.pythonhosted.org/packages/88/ee/2a5c2aa2c32016a226ca25d3e1056a8102ea6e1fe308bf50213586635400/lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105", size = 5005790, upload-time = "2026-04-18T04:33:01.272Z" }, - { url = "https://files.pythonhosted.org/packages/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" }, - { url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" }, - { url = "https://files.pythonhosted.org/packages/55/ba/eeef4ccba09b2212fe239f46c1692a98db1878e0872ae320756488878a94/lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32", size = 5350121, upload-time = "2026-04-18T04:33:09.365Z" }, - { url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" }, - { url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" }, - { url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" }, - { url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" }, - { url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3a/ac3f99ec8ac93089e7dd556f279e0d14c24de0a74a507e143a2e4b496e7c/lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f", size = 3596289, upload-time = "2026-04-18T04:27:42.819Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a7/0a915557538593cb1bbeedcd40e13c7a261822c26fecbbdb71dad0c2f540/lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366", size = 3997059, upload-time = "2026-04-18T04:27:46.764Z" }, - { url = "https://files.pythonhosted.org/packages/92/96/a5dc078cf0126fbfbc35611d77ecd5da80054b5893e28fb213a5613b9e1d/lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819", size = 3659552, upload-time = "2026-04-18T04:27:51.133Z" }, - { url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" }, - { url = "https://files.pythonhosted.org/packages/3f/58/25e00bb40b185c974cfe156c110474d9a8a8390d5f7c92a4e328189bb60e/lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d", size = 4617892, upload-time = "2026-04-18T04:32:01.78Z" }, - { url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" }, - { url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" }, - { url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" }, - { url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" }, - { url = "https://files.pythonhosted.org/packages/6b/10/e9842d2ec322ea65f0a7270aa0315a53abed06058b88ef1b027f620e7a5f/lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9", size = 5341578, upload-time = "2026-04-18T04:33:44.596Z" }, - { url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" }, - { url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" }, - { url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" }, - { url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" }, - { url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" }, - { url = "https://files.pythonhosted.org/packages/5e/73/16596f7e4e38fa33084b9ccbccc22a15f82a290a055126f2c1541236d2ff/lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62", size = 3596938, upload-time = "2026-04-18T04:31:56.206Z" }, - { url = "https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16", size = 3995728, upload-time = "2026-04-18T04:31:58.763Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e8/c358a38ac3e541d16a1b527e4e9cb78c0419b0506a070ace11777e5e8404/lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d", size = 3658372, upload-time = "2026-04-18T04:32:03.629Z" }, - { url = "https://files.pythonhosted.org/packages/eb/45/cee4cf203ef0bab5c52afc118da61d6b460c928f2893d40023cfa27e0b80/lxml-6.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8", size = 8576713, upload-time = "2026-04-18T04:32:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a7/eda05babeb7e046839204eaf254cd4d7c9130ce2bbf0d9e90ea41af5654d/lxml-6.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9", size = 4623874, upload-time = "2026-04-18T04:32:10.755Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e9/db5846de9b436b91890a62f29d80cd849ea17948a49bf532d5278ee69a9e/lxml-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03", size = 4949535, upload-time = "2026-04-18T04:34:06.657Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ba/0d3593373dcae1d68f40dc3c41a5a92f2544e68115eb2f62319a4c2a6500/lxml-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb", size = 5086881, upload-time = "2026-04-18T04:34:09.556Z" }, - { url = "https://files.pythonhosted.org/packages/43/76/759a7484539ad1af0d125a9afe9c3fb5f82a8779fd1f5f56319d9e4ea2fd/lxml-6.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c", size = 5031305, upload-time = "2026-04-18T04:34:12.336Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b9/c1f0daf981a11e47636126901fd4ab82429e18c57aeb0fc3ad2940b42d8b/lxml-6.1.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28", size = 5647522, upload-time = "2026-04-18T04:34:14.89Z" }, - { url = "https://files.pythonhosted.org/packages/31/e6/1f533dcd205275363d9ba3511bcec52fa2df86abf8abe6a5f2c599f0dc31/lxml-6.1.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086", size = 5239310, upload-time = "2026-04-18T04:34:17.652Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8c/4175fb709c78a6e315ed814ed33be3defd8b8721067e70419a6cf6f971da/lxml-6.1.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f", size = 5350799, upload-time = "2026-04-18T04:34:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/6ffdebc5994975f0dde4acb59761902bd9d9bb84422b9a0bd239a7da9ca8/lxml-6.1.0-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292", size = 4697693, upload-time = "2026-04-18T04:34:23.541Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f1/565f36bd5c73294602d48e04d23f81ff4c8736be6ba5e1d1ec670ac9be80/lxml-6.1.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb", size = 5250708, upload-time = "2026-04-18T04:34:26.001Z" }, - { url = "https://files.pythonhosted.org/packages/5a/11/a68ab9dd18c5c499404deb4005f4bc4e0e88e5b72cd755ad96efec81d18d/lxml-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad", size = 5084737, upload-time = "2026-04-18T04:34:28.32Z" }, - { url = "https://files.pythonhosted.org/packages/ab/78/e8f41e2c74f4af564e6a0348aea69fb6daaefa64bc071ef469823d22cc18/lxml-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb", size = 4737817, upload-time = "2026-04-18T04:34:30.784Z" }, - { url = "https://files.pythonhosted.org/packages/06/2d/aa4e117aa2ce2f3b35d9ff246be74a2f8e853baba5d2a92c64744474603a/lxml-6.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f", size = 5670753, upload-time = "2026-04-18T04:34:33.675Z" }, - { url = "https://files.pythonhosted.org/packages/08/f5/dd745d50c0409031dbfcc4881740542a01e54d6f0110bd420fa7782110b8/lxml-6.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43", size = 5238071, upload-time = "2026-04-18T04:34:36.12Z" }, - { url = "https://files.pythonhosted.org/packages/3e/74/ad424f36d0340a904665867dab310a3f1f4c96ff4039698de83b77f44c1f/lxml-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585", size = 5264319, upload-time = "2026-04-18T04:34:39.035Z" }, - { url = "https://files.pythonhosted.org/packages/53/36/a15d8b3514ec889bfd6aa3609107fcb6c9189f8dc347f1c0b81eded8d87c/lxml-6.1.0-cp314-cp314-win32.whl", hash = "sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f", size = 3657139, upload-time = "2026-04-18T04:32:20.006Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a4/263ebb0710851a3c6c937180a9a86df1206fdfe53cc43005aa2237fd7736/lxml-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120", size = 4064195, upload-time = "2026-04-18T04:32:23.876Z" }, - { url = "https://files.pythonhosted.org/packages/80/68/2000f29d323b6c286de077ad20b429fc52272e44eae6d295467043e56012/lxml-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946", size = 3741870, upload-time = "2026-04-18T04:32:27.922Z" }, - { url = "https://files.pythonhosted.org/packages/30/e9/21383c7c8d43799f0da90224c0d7c921870d476ec9b3e01e1b2c0b8237c5/lxml-6.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c", size = 8827548, upload-time = "2026-04-18T04:32:15.094Z" }, - { url = "https://files.pythonhosted.org/packages/a5/01/c6bc11cd587030dd4f719f65c5657960649fe3e19196c844c75bf32cd0d6/lxml-6.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d", size = 4735866, upload-time = "2026-04-18T04:32:18.924Z" }, - { url = "https://files.pythonhosted.org/packages/f3/01/757132fff5f4acf25463b5298f1a46099f3a94480b806547b29ce5e385de/lxml-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9", size = 4969476, upload-time = "2026-04-18T04:34:41.889Z" }, - { url = "https://files.pythonhosted.org/packages/fd/fb/1bc8b9d27ed64be7c8903db6c89e74dc8c2cd9ec630a7462e4654316dc5b/lxml-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9", size = 5103719, upload-time = "2026-04-18T04:34:44.797Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e7/5bf82fa28133536a54601aae633b14988e89ed61d4c1eb6b899b023233aa/lxml-6.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7", size = 5027890, upload-time = "2026-04-18T04:34:47.634Z" }, - { url = "https://files.pythonhosted.org/packages/2d/20/e048db5d4b4ea0366648aa595f26bb764b2670903fc585b87436d0a5032c/lxml-6.1.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86", size = 5596008, upload-time = "2026-04-18T04:34:51.503Z" }, - { url = "https://files.pythonhosted.org/packages/9a/c2/d10807bc8da4824b39e5bd01b5d05c077b6fd01bd91584167edf6b269d22/lxml-6.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb", size = 5224451, upload-time = "2026-04-18T04:34:54.263Z" }, - { url = "https://files.pythonhosted.org/packages/3c/15/2ebea45bea427e7f0057e9ce7b2d62c5aba20c6b001cca89ed0aadb3ad41/lxml-6.1.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c", size = 5312135, upload-time = "2026-04-18T04:34:56.818Z" }, - { url = "https://files.pythonhosted.org/packages/31/e2/87eeae151b0be2a308d49a7ec444ff3eb192b14251e62addb29d0bf3778f/lxml-6.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f", size = 4639126, upload-time = "2026-04-18T04:34:59.704Z" }, - { url = "https://files.pythonhosted.org/packages/a3/51/8a3f6a20902ad604dd746ec7b4000311b240d389dac5e9d95adefd349e0c/lxml-6.1.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773", size = 5232579, upload-time = "2026-04-18T04:35:02.658Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d2/650d619bdbe048d2c3f2c31edb00e35670a5e2d65b4fe3b61bce37b19121/lxml-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b", size = 5084206, upload-time = "2026-04-18T04:35:05.175Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8a/672ca1a3cbeabd1f511ca275a916c0514b747f4b85bdaae103b8fa92f307/lxml-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405", size = 4758906, upload-time = "2026-04-18T04:35:08.098Z" }, - { url = "https://files.pythonhosted.org/packages/be/f1/ef4b691da85c916cb2feb1eec7414f678162798ac85e042fa164419ac05c/lxml-6.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690", size = 5620553, upload-time = "2026-04-18T04:35:11.23Z" }, - { url = "https://files.pythonhosted.org/packages/59/17/94e81def74107809755ac2782fdad4404420f1c92ca83433d117a6d5acf0/lxml-6.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd", size = 5229458, upload-time = "2026-04-18T04:35:14.254Z" }, - { url = "https://files.pythonhosted.org/packages/21/55/c4be91b0f830a871fc1b0d730943d56013b683d4671d5198260e2eae722b/lxml-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180", size = 5247861, upload-time = "2026-04-18T04:35:17.006Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ca/77123e4d77df3cb1e968ade7b1f808f5d3a5c1c96b18a33895397de292c1/lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2", size = 3897377, upload-time = "2026-04-18T04:32:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/64/ce/3554833989d074267c063209bae8b09815e5656456a2d332b947806b05ff/lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5", size = 4392701, upload-time = "2026-04-18T04:32:12.113Z" }, - { url = "https://files.pythonhosted.org/packages/2b/a0/9b916c68c0e57752c07f8f64b30138d9d4059dbeb27b90274dedbea128ff/lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac", size = 3817120, upload-time = "2026-04-18T04:32:15.803Z" }, - { url = "https://files.pythonhosted.org/packages/f2/88/55143966481409b1740a3ac669e611055f49efd68087a5ce41582325db3e/lxml-6.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842", size = 3930134, upload-time = "2026-04-18T04:32:35.008Z" }, - { url = "https://files.pythonhosted.org/packages/b5/97/28b985c2983938d3cb696dd5501423afb90a8c3e869ef5d3c62569282c0f/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c", size = 4210749, upload-time = "2026-04-18T04:36:03.626Z" }, - { url = "https://files.pythonhosted.org/packages/29/67/dfab2b7d58214921935ccea7ce9b3df9b7d46f305d12f0f532ac7cf6b804/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de", size = 4318463, upload-time = "2026-04-18T04:36:06.309Z" }, - { url = "https://files.pythonhosted.org/packages/32/a2/4ac7eb32a4d997dd352c32c32399aae27b3f268d440e6f9cfa405b575d2f/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635", size = 4251124, upload-time = "2026-04-18T04:36:09.056Z" }, - { url = "https://files.pythonhosted.org/packages/33/ef/d6abd850bb4822f9b720cfe36b547a558e694881010ff7d012191e8769c6/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037", size = 4401758, upload-time = "2026-04-18T04:36:11.803Z" }, - { url = "https://files.pythonhosted.org/packages/40/44/3ee09a5b60cb44c4f2fbc1c9015cfd6ff5afc08f991cab295d3024dcbf2d/lxml-6.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace", size = 3508860, upload-time = "2026-04-18T04:32:48.619Z" }, +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/da/dbe4dfc01ac226fb0504fad035f4d69f3202f3502e20e68537631daddd96/lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60", size = 8541124, upload-time = "2026-05-18T19:17:11.589Z" }, + { url = "https://files.pythonhosted.org/packages/78/20/f7095ed9fc2c025f9cfe71cc6ec9f1feb05624edc1812423b5f1aecf3d4b/lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d", size = 4602783, upload-time = "2026-05-18T19:17:20.888Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a4/65c63ca98bd129f6cff7b8c2fa48953ab058cc6005b541354e7dd54d8000/lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea", size = 5002687, upload-time = "2026-05-18T19:17:01.738Z" }, + { url = "https://files.pythonhosted.org/packages/96/1d/ab7a5c4b5a394d98a94e2d0fc67bab8297597426770dd4978370fbdaf531/lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074", size = 5155099, upload-time = "2026-05-18T19:17:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b1/07603bfeeb891a2596d5c2a68f7d2f70f7d11c841ebe391412c69c2857b0/lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30", size = 5057225, upload-time = "2026-05-18T19:17:08.117Z" }, + { url = "https://files.pythonhosted.org/packages/7a/16/cb391ee4b90186fa16d9ebcbe3ea96c71b8da3b0686386c8dcbcc3c67d44/lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315", size = 5287643, upload-time = "2026-05-18T19:17:11.507Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d6/b619717f918fd76747448fdbaee0e769edbc70e659b5b5d0112b7020b7a3/lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1", size = 5412445, upload-time = "2026-05-18T19:17:22.182Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/12bc5390ac0a3edeb579d9535e5049a5dda663438728e179d52fb319c33a/lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206", size = 4770864, upload-time = "2026-05-18T19:17:26.851Z" }, + { url = "https://files.pythonhosted.org/packages/0b/59/6500c09da3137f54f020e908d81cfc5ee3e8888e908fd380207afad7c2e6/lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067", size = 5359594, upload-time = "2026-05-18T19:17:32.527Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9b/f64b4cc6b7ebcf75d95af3cde934d254b5f2f10d4163928d838d86b6eb48/lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a", size = 5107713, upload-time = "2026-05-18T19:17:04.402Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/c7388ad5d3a72315d2832dc1458cbf4f2af7f2b990b606ff4876efd04511/lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa", size = 4803973, upload-time = "2026-05-18T19:17:06.545Z" }, + { url = "https://files.pythonhosted.org/packages/3f/22/76197f0bbf165f0b9e75be59be4997e5259cde973f12f098c1b54c7f5d60/lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383", size = 5349925, upload-time = "2026-05-18T19:17:09.743Z" }, + { url = "https://files.pythonhosted.org/packages/24/52/d2a0cfeccb9bcdc47c7ee05cdae5d69b48c9acf20997790a6338bb0d0b3b/lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1", size = 5309825, upload-time = "2026-05-18T19:17:13.831Z" }, + { url = "https://files.pythonhosted.org/packages/19/4a/b30944266776c2f49749ef2445aa7e78898194134b80ad776386f61b56ae/lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a", size = 3598402, upload-time = "2026-05-18T19:17:08.21Z" }, + { url = "https://files.pythonhosted.org/packages/9e/97/33691c66a4d7ec1a5a98e7c909a5b83ee45c7f7ba4cf92b1c4cf26e98079/lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5", size = 4021295, upload-time = "2026-05-18T19:17:28.638Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5f/26a4dd0e12b9456ff7b12a21af5b491eb6629680d1edd73f4140fd386bcf/lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485", size = 3667717, upload-time = "2026-05-19T19:22:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/62/b0/83f481780d1548750b8ce2ec824073deef2f452d9cd1a6faff8507e3d16d/lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2", size = 8526461, upload-time = "2026-05-18T19:17:25.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/30fa0f808002c7329397bfbb24e306789c0b29f04aa5842c07b174b4216f/lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d", size = 4595375, upload-time = "2026-05-18T19:17:34.555Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d2/edb71cf0e561581a7c5eb2626244320eb04e9f8ce6d563184fd668b45073/lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510", size = 4923654, upload-time = "2026-05-18T19:17:42.917Z" }, + { url = "https://files.pythonhosted.org/packages/4c/77/1bc7eeb0de4577d783fb625aa092cc9357883bba35845a3666bf1259f3dc/lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a", size = 5067921, upload-time = "2026-05-18T19:17:49.175Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3c/c0690d74bd2bc17bc03b5b0d093569ead597dd0bfa088bf99eef8c24e19c/lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d", size = 5002456, upload-time = "2026-05-18T19:17:59.715Z" }, + { url = "https://files.pythonhosted.org/packages/66/8d/d1b3271af0c0f1e27e8472a849e4d2c65bc7766884b9ad2da9e76e145c88/lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8", size = 5202776, upload-time = "2026-05-18T19:18:08.924Z" }, + { url = "https://files.pythonhosted.org/packages/7a/45/689824ffb237fd10125ad273f32b28ff04dc6203c2822c85ff65a93df65e/lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009", size = 5329945, upload-time = "2026-05-18T19:18:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c0/ef73af53767e958fd87d437c170f272e2f6e6c0f854939f133a895f1e711/lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6", size = 4659237, upload-time = "2026-05-18T19:18:18.657Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5e/e1158e40397585e91cb0472374a1f63d0926a1ddeaa92f13d1a1ffe306d5/lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8", size = 5265904, upload-time = "2026-05-18T19:18:24.883Z" }, + { url = "https://files.pythonhosted.org/packages/a0/16/8687e5d1400ed1c0bc41dace232ebb7553952b618ea1f2e5fb6e2cfbbe23/lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83", size = 5045225, upload-time = "2026-05-18T19:17:20.073Z" }, + { url = "https://files.pythonhosted.org/packages/ca/18/d877bd1ae2e5ffdfd4836565aba350db31feb2f2656d6ce70316ed66a05e/lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6", size = 4712721, upload-time = "2026-05-18T19:17:40.512Z" }, + { url = "https://files.pythonhosted.org/packages/44/4d/1f44fd1d770b10dacbf6b5c6e520f4d6e0708744930f719dc04e67cab981/lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c", size = 5252549, upload-time = "2026-05-18T19:17:51.236Z" }, + { url = "https://files.pythonhosted.org/packages/64/5d/1d66b84f850089254c230ef6ea6b267a5a54e2e179a5d960036a05d501d7/lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08", size = 5226877, upload-time = "2026-05-18T19:18:00.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/00/84c4b5302d42a2d0184f38d538c8a197f33b52a50bd4f7bcfe990bce3036/lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621", size = 3594072, upload-time = "2026-05-18T19:17:12.714Z" }, + { url = "https://files.pythonhosted.org/packages/61/9d/2e2f7d876349f45e0f3e29f72da311668853d59b58d473a2dea4f0160135/lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28", size = 4025469, upload-time = "2026-05-18T19:17:50.566Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d5/570e6390e4110331e6208b2ba83d1482cc9146808ee118b22824a34c1070/lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b", size = 3667640, upload-time = "2026-05-19T19:22:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" }, + { url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" }, + { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" }, + { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" }, + { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" }, + { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" }, + { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" }, + { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" }, + { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" }, + { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" }, + { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" }, + { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" }, + { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" }, + { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" }, + { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" }, + { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" }, + { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" }, + { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" }, + { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" }, + { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" }, + { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" }, + { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" }, + { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" }, + { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" }, + { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" }, + { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/b5/32/86a3f0f724a3a402d4627937a7fc27b160e45e7012b4adf47f6e1e844511/lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e", size = 3930127, upload-time = "2026-05-18T19:19:02.27Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/d832e82af08723761556d004b1d04d281c09f9a8cecd7d3148548c9941a3/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004", size = 4210769, upload-time = "2026-05-18T19:20:41.427Z" }, + { url = "https://files.pythonhosted.org/packages/6d/39/0dc5949f759ed7d951e0bb8c2f2d9d7aca1908d22352fa84a8afd2ea54af/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e", size = 4318163, upload-time = "2026-05-18T19:20:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/e6/fb/8ab3845fe046ba4cbf74536bcf6801a774b7caf4350de1c5d37f1f0a9e90/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2", size = 4250945, upload-time = "2026-05-18T19:20:47.385Z" }, + { url = "https://files.pythonhosted.org/packages/68/1b/7553ab136894374ffae8851ec06f98f511cd8e66246e41b6be059d0a7289/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf", size = 4401664, upload-time = "2026-05-18T19:20:50.489Z" }, + { url = "https://files.pythonhosted.org/packages/db/a4/441aee36c6f6b249823d20fd91f9be9ab89d7c5a8ae542a4a4ca6d342d56/lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84", size = 3508989, upload-time = "2026-05-18T19:18:38.158Z" }, ] [[package]] @@ -2186,18 +2235,19 @@ wheels = [ name = "matplotlib" version = "3.10.9" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] dependencies = [ { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "cycler" }, - { name = "fonttools" }, - { name = "kiwisolver" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "pyparsing" }, - { name = "python-dateutil" }, + { name = "cycler", marker = "python_full_version < '3.11'" }, + { name = "fonttools", marker = "python_full_version < '3.11'" }, + { name = "kiwisolver", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pillow", marker = "python_full_version < '3.11'" }, + { name = "pyparsing", marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } wheels = [ @@ -2257,9 +2307,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, ] +[[package]] +name = "matplotlib" +version = "3.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler", marker = "python_full_version >= '3.11'" }, + { name = "fonttools", marker = "python_full_version >= '3.11'" }, + { name = "kiwisolver", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pillow", marker = "python_full_version >= '3.11'" }, + { name = "pyparsing", marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/24/080c99d223d158d3a8902769269ab6da5b50f7a0e6e072513907e02b7a6c/matplotlib-3.11.0.tar.gz", hash = "sha256:68c0c7be01b30dcca3638934f7f591df73401235cbdbf0d1ab1c71e7db7f8b57", size = 33251176, upload-time = "2026-06-12T02:29:15.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/a2/78f662f1b18968531f67d3fcde1b7ea8496920bacd4f16ddb5b79d112e46/matplotlib-3.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f857524b442f0f36e641868ce2171aafa88cb0bc0644f4e1d8a5df9b32649fef", size = 9436261, upload-time = "2026-06-12T02:27:34.161Z" }, + { url = "https://files.pythonhosted.org/packages/5e/92/044f1de43901310202f4c79acf4f141be53b2ca8d8380e2fcefb3d523a75/matplotlib-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:57baa92fdc82948ed716eae6d2579d4d6f40965cd8d2f416755b4a72580a3233", size = 9264669, upload-time = "2026-06-12T02:27:37.413Z" }, + { url = "https://files.pythonhosted.org/packages/53/f4/f0b4f9ba7ec14a7af8151f3ad71ecfe3561e6ba38cfab1db3681ba4ca112/matplotlib-3.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630eee0e67d35cce2019a0e670719f4816e3b86aff0fa72729f6c69786fceb45", size = 10021076, upload-time = "2026-06-12T02:27:39.926Z" }, + { url = "https://files.pythonhosted.org/packages/d7/33/4d679c6dcd594a156542080ac907ddccf7b09ca11655c4b28eca8e9ee5da/matplotlib-3.11.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5106c444d0bf966eee2853548c03772af4ab7199118e086c62fbac8ccb07c055", size = 10828999, upload-time = "2026-06-12T02:27:42.433Z" }, + { url = "https://files.pythonhosted.org/packages/07/74/0a3683802037d8cd013144d77c247219b47f2aabace6fdde74faa12bacf7/matplotlib-3.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d7aea652b58e686444079be3376ef546bffa1eee9b9bb9c472b9fcf6cf410d3", size = 10913103, upload-time = "2026-06-12T02:27:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/970fcbf381e82ec66fdf5da8ea76e2e9240f61a24011ce9fd1d42c37ac2d/matplotlib-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:70a5b3e9a5dab708c0f039709ae7c68d5b4d254e291ef76492cdba230c8bb5e4", size = 9310945, upload-time = "2026-06-12T02:27:46.867Z" }, + { url = "https://files.pythonhosted.org/packages/14/4e/6e7cfed23611265ded53806852343b5c59339e506e84c474a9b5afc3b249/matplotlib-3.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:3d68266213e73823ac3be90615bab0cf31f88851e114cdb1dd25dacf3b01e1a7", size = 8999304, upload-time = "2026-06-12T02:27:48.798Z" }, + { url = "https://files.pythonhosted.org/packages/da/17/f5276b496c61477a6c4fc5e7401f4bfe1c2e5ef7c6cd67896f2ade3809cb/matplotlib-3.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06b5872e9cf11adc8f589ded3ce11bc3e1061ad498259664fabc1f6615beb918", size = 9449976, upload-time = "2026-06-12T02:27:50.989Z" }, + { url = "https://files.pythonhosted.org/packages/82/34/bdd77418adb2178a1d59f044bd67bfebb115896e91b840b8a197eb3f4f4e/matplotlib-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0515d495124be3124340e59f164d901ed4484e2246a5b74cfa483cac3b80bd97", size = 9279307, upload-time = "2026-06-12T02:27:53.247Z" }, + { url = "https://files.pythonhosted.org/packages/94/95/7f522393c88313336b20d70fc849555757b2e5febc22b83b3a3f0fd4bce9/matplotlib-3.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be5f93a1d21981bfb802ded0d77a0caa92d4342a47d45754fac77e314a506344", size = 10031353, upload-time = "2026-06-12T02:27:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/87/ce/8f25a0e3186aefd61913e7467d1b999465bcd0d0c03ac695c1b26ca559b7/matplotlib-3.11.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41635d7909d19e52e924a521dde6d8f670b0f53ab1d0e8c331fa831554f681d1", size = 10839232, upload-time = "2026-06-12T02:27:57.746Z" }, + { url = "https://files.pythonhosted.org/packages/85/c2/db15da2bbdf9e3ca66df7db8e2c33a1dfed67be24a24d2c878efaaff01d6/matplotlib-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94f5000f67ca9faa300863ea17f8bce9175cb67b88bec4bc7780502d53dd7c9e", size = 10923899, upload-time = "2026-06-12T02:28:00.223Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/a58a4443a4d052a4ea77557478336aefc26c7981f6408d37adba763aa758/matplotlib-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac6f1ef39f3d0f9e2463303013094992cdbe0f85f43bc54155bc472b2042768e", size = 9329528, upload-time = "2026-06-12T02:28:02.27Z" }, + { url = "https://files.pythonhosted.org/packages/61/0f/4b669589d47733b97ab9df4b58d6fc1e68acb5ea42a928dc7cbdd6bf5871/matplotlib-3.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:9dd11fb612ce7bc60b1de5b4fc87ff959d22317b5de42aabf392f66f97af22eb", size = 9003413, upload-time = "2026-06-12T02:28:04.49Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/aa47f156b061d14c98b906f76c428507397708ec63ff94f410ae1752b426/matplotlib-3.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce3b839b34ae1f430b4616893a2945a2999debaa7e94e7e29a2a8bbf286f7b5", size = 9450532, upload-time = "2026-06-12T02:28:06.769Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4f/5a9eb0375e81413953febf8af7b012a6b6357f53438a15c4f5ad86c6bbb5/matplotlib-3.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:373db8f91214e8ccaf35ac833cc1dd59dd961e148bbd55dd027141591dde1313", size = 9279760, upload-time = "2026-06-12T02:28:09.152Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c0/1117d53077e3ac3152503a84e9cf7a5c239576805ee71276e80c2aaa7471/matplotlib-3.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be152b7570324dc8d01574cc9474dd2d803237acf528bcbb5b211fa347461a09", size = 10031623, upload-time = "2026-06-12T02:28:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/92/7e/e937138daffad65b71bf831a377809dcbc830fb4f31a31e067dc1faa2575/matplotlib-3.11.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:126f256df600652d7e4b394cf3164ff75210a00038f287c95a012a6f58d0e83f", size = 10839372, upload-time = "2026-06-12T02:28:14.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c2/438ecc197ffb8023b6b9922915542f2172f5fd45b76703b0b4fc47322243/matplotlib-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03acfeddf87b0dddb11b081ef7740ad445a3ca8bcb6b8e3011b08f2cf802b75c", size = 10924099, upload-time = "2026-06-12T02:28:16.383Z" }, + { url = "https://files.pythonhosted.org/packages/40/2e/395883da416f378b3ed2c9f3e843ac477eae1ce731b671b79adaa6f0bacd/matplotlib-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:ab3722f04f3ff34c23b5012c5873d2894174e06c3822fcdac3610965a5ac7d06", size = 9329727, upload-time = "2026-06-12T02:28:18.581Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/2c388956abf8bf392dfb5b8917c502f1082df6a941b781ab8c8e5ba2474b/matplotlib-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:c945824670fb8915b4ac879e5e61f3c58e0913022f70a0de4c082b17372f8771", size = 9003506, upload-time = "2026-06-12T02:28:20.474Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/34454baa44da7975ada82e9aea37105ec47059514dc967d3be14426ba8dc/matplotlib-3.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3489c3dc487669b4a980bc3068f87856de7a1564248d3f6c629efb2a58b03f24", size = 9499838, upload-time = "2026-06-12T02:28:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c3/98fe79a398cf232219f090163a7fa7e6766e9f2e0ad26df54d6f8934d8ee/matplotlib-3.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6a98f5476ce784a50ce09998f4ae1e6a9f25043cef8a480c98949902eda74620", size = 9332298, upload-time = "2026-06-12T02:28:24.796Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/b4b7c33151e74e5c802f3cde1ba807ebfc38401e329b44e215a5888dd76d/matplotlib-3.11.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:565af866fd63e4bd3f987d580afe27c44c2552a3b3305f4ecbb85133601ea6f3", size = 10045491, upload-time = "2026-06-12T02:28:27.141Z" }, + { url = "https://files.pythonhosted.org/packages/71/28/394548efd68354110c1a1be11fe6b6e559e06d1a23da35908a0e316c55a9/matplotlib-3.11.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6b3e64dea5062c570f04358e2711859f3531b459f29516274fbad889079e4f3", size = 10857059, upload-time = "2026-06-12T02:28:29.222Z" }, + { url = "https://files.pythonhosted.org/packages/c8/44/e7922e6e2a4d63bdfbc9dc4a53e3850ab438d46cf42e6779bb15ec92c948/matplotlib-3.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:942b37c5db1899610bd1543ce8e13e4ecff9a4633e7f63bb6aa9205d2644ebd1", size = 10939576, upload-time = "2026-06-12T02:28:31.66Z" }, + { url = "https://files.pythonhosted.org/packages/3d/be/b1ca96003a441d619b727fee21d671fdff7a5ce2f1bb797b2521aa2f679a/matplotlib-3.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c08e649a6313e1291e713623b97a38e5bb4aa580b2a100a94a3309bc6b9c8eb3", size = 9379519, upload-time = "2026-06-12T02:28:33.888Z" }, + { url = "https://files.pythonhosted.org/packages/e3/72/4bf3b91821c34596dd6a7bdac5836d94f744144c8208939ef49d8ec43f7e/matplotlib-3.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2746cd2c113742ff6ce37a864c5ac5fd7aa644568f445e66166e457ac78e40e0", size = 9055456, upload-time = "2026-06-12T02:28:35.878Z" }, + { url = "https://files.pythonhosted.org/packages/57/52/a94102ac99eb78e2fe9b826674f9ef9ee23327110ea6ab4776c1b4eb6209/matplotlib-3.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3338e3e3de128cf50d0d2fb92a122815daf9c755bd882a474343c05f8fd7ec79", size = 9452137, upload-time = "2026-06-12T02:28:37.93Z" }, + { url = "https://files.pythonhosted.org/packages/7c/03/b8cdb625a21f710dfa11bbca1f48fb4057d2c0286975f8b415bf80942c99/matplotlib-3.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:25c2e5455efd8d99f41fb79871a31feb7d301569642e332ec58d72cfe9282bc3", size = 9281514, upload-time = "2026-06-12T02:28:40.028Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2d/4e1240ea82ee197dfb3851e71f71c87eeeb975f1753b56a0588e4e80739a/matplotlib-3.11.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9695457a467ff86d23f35037a43deb6f1134dd6d3e2ac8ce1e2087cff09ffb9", size = 10843005, upload-time = "2026-06-12T02:28:42.39Z" }, + { url = "https://files.pythonhosted.org/packages/29/dc/6377ecfaa5fef79430f74a1a16638b4e2aa30d4692bae2c19f9d76fe3b01/matplotlib-3.11.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19c16c61dea63b3582918503e6b294193961261d9daa806d4ae2151f1ad05430", size = 11127459, upload-time = "2026-06-12T02:28:44.483Z" }, + { url = "https://files.pythonhosted.org/packages/6f/41/795c405aa7560443a3b01309424cde4a1113b85c90b8a63417444a749617/matplotlib-3.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2d72ea8b7924f3cb955e61518d21e43b3df1e6c8a793b480a0c1214f185d30ba", size = 10925160, upload-time = "2026-06-12T02:28:46.564Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f7/3a9e6389a7cfaeff76c56e40c2dabcb13110e21e82f837228c834ebe748c/matplotlib-3.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:1c02da0a629dfa9debf52725ea06866b74c1fb70a895bae05e4493d34074f9f2", size = 9485186, upload-time = "2026-06-12T02:28:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c0/396478ee7cf2091d182db8b4a8695f6a37f1ddb978989cf9dbb84cd5c123/matplotlib-3.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aa55d73b3117d4b07f959cd9eb6f69b375d8df3414139c479388e551aa5d999d", size = 9160349, upload-time = "2026-06-12T02:28:51.382Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6f/1c3bd51bb2b34eaacdcf3c3d859dbb357f952fc8020c617dc118ad7c9e38/matplotlib-3.11.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9d8c6e7cd2f0ddf11d8d92e520dd1d9d2abb0cf6ac8831e338666c81e905847", size = 9500921, upload-time = "2026-06-12T02:28:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/4d861d0121840cb1a3fd4a10deb211efd6fccd481ed23e553f31f4f4da4a/matplotlib-3.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:be050fcf32f729eda99f7f75a80bf67612ce16ab9ac1c23a387dcaede95cb70e", size = 9332190, upload-time = "2026-06-12T02:28:55.623Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/22f6bc35711a0b5639a784e74e653e77c86210bd4304449dd399a482f74e/matplotlib-3.11.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfabef0230d0697aa0d717385194dd41162e00207a68bf4abf94c2bf4c27dca0", size = 10854181, upload-time = "2026-06-12T02:28:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7e/9a9eaca731a2939589da520f0ebe8fd8753d0f51fca98c7d20af6dbe261a/matplotlib-3.11.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1644db30e759199443493ac5e5caec24fdb775a8f6123021f85ba47c4133c3cb", size = 11137715, upload-time = "2026-06-12T02:29:00.555Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f9/9b030b6088354acb0296871bb624b25befc1c42509d3c6cd17420c83a5b8/matplotlib-3.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15b0d160079cb10699a0e98b5989c70677b2df7cacdc62af67c30f2facec46d9", size = 10939427, upload-time = "2026-06-12T02:29:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/59/94/6b273eaee4ee250863567d100865da61a5c1527fa67f527b7ed22e0dd29c/matplotlib-3.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:446307e6b04b57b1f1239e228a1ec2af0d589a1008cebc3dfa3f5441d095cfb6", size = 9535809, upload-time = "2026-06-12T02:29:04.994Z" }, + { url = "https://files.pythonhosted.org/packages/60/95/1d36bddf2b7e2692c1540e78a6e5bc88bc1496b137e3e35a611f91b65ac3/matplotlib-3.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:652fb5696271d4c50f196d22a5ff4f8e4444c74f847423570d7dc0aa2bbd0159", size = 9209226, upload-time = "2026-06-12T02:29:07.033Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c2/f5da6cd37ed6871f5c9b3c0507ddb69f14d6c36fac4541e4e0c60cb8cdfc/matplotlib-3.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81ae77077a1e16d37a5b61096ccb07c8d90a99b518fa8256b8f21578932f2f62", size = 9434094, upload-time = "2026-06-12T02:29:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/f8/07/56f66906e0f87a0c6d0d0acbd34dbc9432b1931d8f26ef618bd6f92932a9/matplotlib-3.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ddef37840695f5eef65f9f070fe2d2f510f584c2156203f9f622a5b0584efffd", size = 9262183, upload-time = "2026-06-12T02:29:11.283Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d8/c4ecab06b7ea36a570c4f3bd2d48d1799fd5d9174470e45c2194199431e7/matplotlib-3.11.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf662e5ac5707658cb931e19972c4bd99f7b4f8b7bf79d3c821d239fa6b71e64", size = 10015653, upload-time = "2026-06-12T02:29:13.251Z" }, +] + [[package]] name = "mcp" -version = "1.27.1" +version = "1.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2277,9 +2398,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/3c/347cf965d313f5d41764e7d46bea6ffe7d9ef13b983cc429b0340962a082/mcp-1.27.2.tar.gz", hash = "sha256:8e02db104096d1c25b28e64bde29a5c32b31bc241710213e12fd4d84985bdfef", size = 621116, upload-time = "2026-05-29T17:16:04.039Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, + { url = "https://files.pythonhosted.org/packages/c9/11/252c6f971dc4f16af1d98a1c469d8ba523aab00d1bb76b4d3bc1ff32eacc/mcp-1.27.2-py3-none-any.whl", hash = "sha256:d6ff5160c6ca65d93013626efb3fc249de683c30b2d8570755ceddd490344de5", size = 220498, upload-time = "2026-05-29T17:16:02.442Z" }, ] [[package]] @@ -2293,63 +2414,84 @@ wheels = [ [[package]] name = "msgpack" -version = "1.1.2" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, - { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, - { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, - { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, - { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, - { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, - { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, - { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, - { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, - { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, - { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, - { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, - { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, - { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, - { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, - { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, - { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, - { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, - { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, - { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, - { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, - { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, - { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, - { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, - { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, - { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, - { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, - { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, - { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, - { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/92/23/6139781ca7aadf656fa8e384fa84693ffb13f299e6931b6526427fe5e297/msgpack-1.2.0.tar.gz", hash = "sha256:8e17af38197bf58e7e819041678f6178f4491493f5b8c8580414f40f7c2c3c41", size = 183017, upload-time = "2026-06-11T04:16:10.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/52/fed22bca455ff3ed28c0ee0d1117398b7cb3ce440270050e85b09240fa8d/msgpack-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ed8c9495a0f12d17a2b4b69e23f895b88f26aabe40911c86594d3fbddecfff08", size = 82473, upload-time = "2026-06-11T04:14:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/3b/09/0b54d386024a9fa2073135212c11d1e83b059d98459d943d5a82ba9dcdc9/msgpack-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7384859c90b45a28a4b31aa50b49cca84504c9f27df459cea6e072627650dcb", size = 82150, upload-time = "2026-06-11T04:14:39.985Z" }, + { url = "https://files.pythonhosted.org/packages/44/ba/c6310a6f37e9bf9279b492640ec425e6f6e68a94e4cac4782ab518b05d64/msgpack-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63b35e8e65f04ff7ad5c9c70885da587c74f51e4b4eb3db624eac6d250e8cf59", size = 398355, upload-time = "2026-06-11T04:14:41.493Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1b/f4bad0e9dea608b14d36065c44e347e4b10c0392f92cca441496cc0598ef/msgpack-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004c5a02acd3eca4e15e1ae7b461c32e3711105a28b1ad78be2f6facff4c523", size = 405162, upload-time = "2026-06-11T04:14:42.957Z" }, + { url = "https://files.pythonhosted.org/packages/63/34/4653bc7f426bd6ce9803f75133aa362232639e5adb8c6b99550107c71ed5/msgpack-1.2.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e2032dacb0a973fcbf7bd088415a369dae31c5af40e199d234806be22e86765", size = 372720, upload-time = "2026-06-11T04:14:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/8c607e10db2225af52107ffa918280483248363819fecb4437a35a1f4ae2/msgpack-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c1feb100651fbe4b39826207cb20af065dfbfbfa43b1bafd7eaa2252abf7acfd", size = 390946, upload-time = "2026-06-11T04:14:46.054Z" }, + { url = "https://files.pythonhosted.org/packages/96/05/c4cb5fb30569cff4b4c7be4574adddb0faf7faaf3049bbab000b6f07da5b/msgpack-1.2.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:82487709d4c597d252311a65370220675fb1cc859e7da9269a3060c03ac02cf6", size = 374062, upload-time = "2026-06-11T04:14:47.817Z" }, + { url = "https://files.pythonhosted.org/packages/40/d7/b51b11e58277e6b678ba5a2f6608f88fdb0778973391a39d7f1a385f5bde/msgpack-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0268c67a74f5f913f545a0fdbbfaa3f6ebcf23b4c3209bb99704a2ea87e13f90", size = 405458, upload-time = "2026-06-11T04:14:49.618Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/9eca2961be302a6fc77a3fcb15faec749e325c9f0a8fe9c4c4576fc2cad5/msgpack-1.2.0-cp310-cp310-win32.whl", hash = "sha256:7df87173b0e13ddd134919731f13525dbbf75204145597decf1cb86887ebb492", size = 64010, upload-time = "2026-06-11T04:14:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/55b14ae13ed056ed35364ff71144c6a12af25227c20093045a945d08273a/msgpack-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:6371edb47788fbfd8a22016f9a97b5616dd9849bc50abcbb8e82d38f71efa096", size = 69863, upload-time = "2026-06-11T04:14:52.376Z" }, + { url = "https://files.pythonhosted.org/packages/ee/23/35de3182a647fcc84ab304160169edfa5dac7bbd8913fbed0a505ddc0d55/msgpack-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec35cd3f127f50806aa10c3f74bf27b749f13ddf1d2217964ada8f38042d1653", size = 82368, upload-time = "2026-06-11T04:14:53.57Z" }, + { url = "https://files.pythonhosted.org/packages/aa/79/8d9bfdab933b1c7a02aba9518605a81aa30d38e9efd4915ec1a6b2d55778/msgpack-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:317eb298297121bfad9173d748124a04a36af27b6ac39c2bbc1db1ce57608dcf", size = 82095, upload-time = "2026-06-11T04:14:54.784Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e1/b5accbc1354edbcee107fb35ec247db0547e91c3f90e4fabdeaee500a5a6/msgpack-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50fe6434de89073273026dd032a62e8b63f8857a261d7a2df5b07c9e72f3a8f7", size = 413818, upload-time = "2026-06-11T04:14:56.1Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/1141cbbf7118d525834f20dcd614d1b85f1f2ffd33bc2a5ce710e6dd2516/msgpack-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106c6d333ff3d4eda075b7d4b9695d1752c5bcc635e40d0dbaf4e276c9ed80e1", size = 423790, upload-time = "2026-06-11T04:14:57.509Z" }, + { url = "https://files.pythonhosted.org/packages/04/e7/9582f2bd4d7546139fe297740de49bd1f7ef2d195eb0bb9fa5efeee88158/msgpack-1.2.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:67055a611e871cb1bd0acb732f2e9f64ca8155ca0bba1d0a5bb362e7209e5541", size = 387521, upload-time = "2026-06-11T04:14:59.08Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/5aadd08ff068bfd42e2ac0be6a20aa9819965df8622e87c1f0c6119c1c22/msgpack-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ceec7f8e633d5a4b4a32b0416bef90ee3cd1017ea36247f705e523072e576119", size = 406324, upload-time = "2026-06-11T04:15:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/39/ee/3041564f0cc4c2fe7c53315aec0edf3d84807fc9b9ea714e6ac07dbdb1db/msgpack-1.2.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7ec5851160a3c2c0f77d68ddec620318cd8e7d88d94f9c058190e8ce0dfa1d31", size = 384242, upload-time = "2026-06-11T04:15:02.121Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/de94b3dbc266229f4c2ce84485eeb221220351b7f1931029e875995bb232/msgpack-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd7140f7b09dbe1984a0dff3189375d840247e3e4cf4ac45c5a499b3b599c8d2", size = 420392, upload-time = "2026-06-11T04:15:03.692Z" }, + { url = "https://files.pythonhosted.org/packages/f7/5d/c4a3fde69a292eecb202caaa87c29df7728644a65118614b821bcaddc05a/msgpack-1.2.0-cp311-cp311-win32.whl", hash = "sha256:cbfd54018d386da0951c7a2be13de0f58559d251313e613b2155e52ed1cbd8f1", size = 63976, upload-time = "2026-06-11T04:15:05.355Z" }, + { url = "https://files.pythonhosted.org/packages/18/fa/df47f83115375e7717c985265a30f3ba096c5331518e28fb647b55c46d31/msgpack-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:653373c4614c31463ba486a67776e4bb396af289921bd5353e209534b71467fa", size = 70273, upload-time = "2026-06-11T04:15:06.529Z" }, + { url = "https://files.pythonhosted.org/packages/54/d1/ffd02e54c064aa73b6b53aa08171f92dc406727077ff275d7050c6aca28a/msgpack-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:7a260aea1e5e7d6c7f1d9284c7360d29021627b61dc4dd7df144b81210810537", size = 64783, upload-time = "2026-06-11T04:15:07.677Z" }, + { url = "https://files.pythonhosted.org/packages/44/07/dcb13f37e670257c8d0e944f116c799c34ac6968ecb48c83619f7e91d8b5/msgpack-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2d6047ccd11a12c96a69f2bfe026471abef67334c3d0494a93e5310e45140a2", size = 82888, upload-time = "2026-06-11T04:15:08.992Z" }, + { url = "https://files.pythonhosted.org/packages/84/5f/6643b2a6a36ca4bc73c7674831be1d4d581cceecc7eb019dba1915951739/msgpack-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0347e3ac0dfee99086d3b68fe959da3f5f657c0019ddbaeaaa259a85f8603422", size = 82223, upload-time = "2026-06-11T04:15:10.182Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c8/9e1668b9897358e5ab39a18142e38be3cf15807e643757782da9f4a53cb3/msgpack-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25552ff1f2ff3dc8333e27eabb94f702da5929ed0e07969688194a3e9f12e151", size = 409700, upload-time = "2026-06-11T04:15:11.441Z" }, + { url = "https://files.pythonhosted.org/packages/38/ed/b7728573156d70b6b094233b0f38d876fc37340826cf852347ec2c7ca8ca/msgpack-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0d94420d9d52c56568159a69200af7e45eadb29615fa9d09fada140de1c38c7", size = 420090, upload-time = "2026-06-11T04:15:12.868Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f7/5ea755a89868c04f9cdf6d96d2d99da4b3d198af10e76a6082dd0fceccc0/msgpack-1.2.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d16e1f2db4a9eebc07b7cc91898d71e710f2eed8358711a605fee802caff8923", size = 378538, upload-time = "2026-06-11T04:15:14.511Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/126e59332a439c94ffd682c38ca0102b23480e2784b3dac48d8959b0bbac/msgpack-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9cb2e700e85f1e27bbb5c9de6cc1c9a4bc5ac64d5404bdcbcb37a0dc7a947a3", size = 399468, upload-time = "2026-06-11T04:15:16.133Z" }, + { url = "https://files.pythonhosted.org/packages/da/f9/7abcef683a0ad2e5ab3a4940344aad9f20cdf1f42057ecb0982cf55085d6/msgpack-1.2.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:717d0b166dd176a5f786aeafff081f6439680acf5af193eb63e6266c12b04d3d", size = 374212, upload-time = "2026-06-11T04:15:17.536Z" }, + { url = "https://files.pythonhosted.org/packages/27/23/2d62cf0e971678e96f8a3cfa9bd77fb719ddb98da73790f63c53fd847ad8/msgpack-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e87c7a21654d18111eb1a89bd5c42baba42e61887365d9e89585e112b4203f9e", size = 414361, upload-time = "2026-06-11T04:15:18.99Z" }, + { url = "https://files.pythonhosted.org/packages/32/fb/f5c153f614037aaf802d291a4653ba1bb731f56feacba886f7c21c109e56/msgpack-1.2.0-cp312-cp312-win32.whl", hash = "sha256:967e0c891f5f23ab65762f2e5dc95922759c79f1ef99ef4c7e1fdd863e0d0af9", size = 64389, upload-time = "2026-06-11T04:15:20.237Z" }, + { url = "https://files.pythonhosted.org/packages/90/af/8aafce6e5544b43b84cb670aca40c8bea7eb5ae8f42bfcbdc7098739987a/msgpack-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:6c23e33cee28dcffa112ae205661da4636fd7b06bd9ad1559a890623b92d060b", size = 71185, upload-time = "2026-06-11T04:15:21.51Z" }, + { url = "https://files.pythonhosted.org/packages/ba/08/9cc94be1fc1fe3d1379d439326259aef0344274f64623a8138feb54dff68/msgpack-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:6eeb771571f63f68045433b1a35c0256b946f31ed62f006997e40b8ad8b735af", size = 64481, upload-time = "2026-06-11T04:15:22.639Z" }, + { url = "https://files.pythonhosted.org/packages/7d/26/2902c6946ab5c8fe1e46e40842dfc32b8824464ad5cd4725364fd83f7a58/msgpack-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a1d30df1f302f2b7a7404afbac2ab76d510036c34cf34dffb01f704a7288e45", size = 82621, upload-time = "2026-06-11T04:15:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/c9/59/7e6b812629d2f919e586041bffc130e1af32079f71bb20699eed54ed6d92/msgpack-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:581e317112260d8ca488d490cad9290a5682276f309c41c7de237a85ed8799c8", size = 81866, upload-time = "2026-06-11T04:15:25.032Z" }, + { url = "https://files.pythonhosted.org/packages/31/13/8c291196e60aafdbae38f482205d79432297749ac5d412fe638154fb6f1d/msgpack-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6827d12eacc16873eba62408a1b7bbe8ecfb4a8f7ed78a631ae9bae6ad43cf2", size = 405618, upload-time = "2026-06-11T04:15:26.235Z" }, + { url = "https://files.pythonhosted.org/packages/fb/63/68f5d0ea81e167db5f59ddb94dc6f837667062113feff1c73fabf8907061/msgpack-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a186027e4279efa4c8bf06ce30605498d7d0d3af0fba0b9799dce85a3fd4a93c", size = 416468, upload-time = "2026-06-11T04:15:27.732Z" }, + { url = "https://files.pythonhosted.org/packages/73/58/567dddf5c5a2790f673bcd7d80c83466d68e5ee9a9674ebca3db8101c0c8/msgpack-1.2.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a96142c14a11cf1a509e8b9aaf72858a3b742b7613e095ce646913e88ce7bd99", size = 374464, upload-time = "2026-06-11T04:15:29.286Z" }, + { url = "https://files.pythonhosted.org/packages/0d/30/0c2342fc9092e4498045f5f60bca6ccbe4f4d87789778c2300e6fd6efe82/msgpack-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50c220579b68a6085b95408b2eaa486b259520f55d8e363ddc9b5d7ba5a6ac6d", size = 395879, upload-time = "2026-06-11T04:15:30.973Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/9565b29b58ce3c33e177b490478b7aaeb8f726ecaaeda26d815893c1db5a/msgpack-1.2.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4dcb9d12ab100ecacdfaaf37a3d72fe8392eacc7054afc1916b12d1b747c8446", size = 371749, upload-time = "2026-06-11T04:15:32.418Z" }, + { url = "https://files.pythonhosted.org/packages/f2/da/7bade19d60b73e2ef73fb76aaf4504c112a70cb760951b7202a0c64b5111/msgpack-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a804727188ab0ebb237fadb303b743f04925a69d8c3247292d1e33e679767c15", size = 410416, upload-time = "2026-06-11T04:15:34.053Z" }, + { url = "https://files.pythonhosted.org/packages/6d/14/c0c619571c02432208a5977a8dbdd3fc65fe1369f8226ca4b6d08cca87d8/msgpack-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1a1ac6ae1fe23298f79380e7b144c8a454e5d05616b0096584f353ba2d750114", size = 64357, upload-time = "2026-06-11T04:15:35.535Z" }, + { url = "https://files.pythonhosted.org/packages/50/a5/de06718460909aa965737fec4cfe8a15dedc6544a8c55feeb6956fa0d6e3/msgpack-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c3c80949d79578f9dc85fd9fb91edfe6694e8a729cd5744634d59d8455fdde3", size = 71057, upload-time = "2026-06-11T04:15:36.83Z" }, + { url = "https://files.pythonhosted.org/packages/c7/52/73446b0141c94a856e22b787c56709c0815fc34f185326577e15b26d8cfe/msgpack-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fcf8f76fa587c2395fd0057c7232dbf071241f9ad280b235adb7ab585289989e", size = 64490, upload-time = "2026-06-11T04:15:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/35/3d/a7e3cdafa8c0cf36c81e2fa848ec4d30cf089459af45b390ad03f9ce6f49/msgpack-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f854fa1a8b55d75d82ef9a905d9cdbeffdf7897c088f6020bd221867da5e56a5", size = 83032, upload-time = "2026-06-11T04:15:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/ca/aa/53ddfba0e347cc4b484e95f629c5850b9e800ca8390c91ffc604407acf87/msgpack-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e90df581f80f53b372d5d9d9349078d729851a3a0d0bd74f53ccb598d01e45b8", size = 82600, upload-time = "2026-06-11T04:15:40.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/fd/e64c2c776e6dbad0af3c963fe0c0dd1ee1ba09efac478b233ab1db41868f/msgpack-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b276ed50d8ac75d1f134a433ae79af8557d0fa25ee5b4737da533dfc2ce382e8", size = 404342, upload-time = "2026-06-11T04:15:41.87Z" }, + { url = "https://files.pythonhosted.org/packages/1b/60/fb9a08e6ccba882dfd370a5837fe3a07572938fdfe954f0f17fdf3e574b9/msgpack-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:544d972459c92aa32e63b800d07c2d9cf2734a3be29cee3a0b478a622850e9f5", size = 412351, upload-time = "2026-06-11T04:15:43.253Z" }, + { url = "https://files.pythonhosted.org/packages/37/4d/df5c575c274fedc68ac9c6c61d045161899efad2afcdc25138efa7edde69/msgpack-1.2.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a070147cc2cf6b8a891734e0f5c8fe8f70ed8739ab30ba140b058005a6e86af4", size = 373331, upload-time = "2026-06-11T04:15:44.754Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a4/c8b98f8191e985ed2003d87664ce3c95cca41db5d0cf6bf4f54327d32ec8/msgpack-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7685e23b0f51745a751629c31713fbefdef8896b31b2bb38299dfa4ae6c0740c", size = 394654, upload-time = "2026-06-11T04:15:46.423Z" }, + { url = "https://files.pythonhosted.org/packages/d4/49/76f036720a602ea24428cfec5ec806f2487c0380b1bff0a2aa3094e15f87/msgpack-1.2.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b9204daeee8d91a7ae5acf2d2a8e3983be9a3025f38aa21bfaefbd7eea84a7dc", size = 370624, upload-time = "2026-06-11T04:15:48.062Z" }, + { url = "https://files.pythonhosted.org/packages/9f/38/40af3d29232833705a43b0fce0d07425cc280a7b92ab2b29932425b40df4/msgpack-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bfc057248609742ebbabf6bcd27fea4fd99c4980584e613c168c9b002318298f", size = 408038, upload-time = "2026-06-11T04:15:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/30/b2/f140ca450524dff4d8d0eb81eb9ed75f8f3e0b1f12e49c5b01617cfa0b1c/msgpack-1.2.0-cp314-cp314-win32.whl", hash = "sha256:a3faa7edf2388337ae849239878e92f0298b4dab4488e4f1834062f9d0c410c9", size = 65823, upload-time = "2026-06-11T04:15:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/4d/13/6517bf966b841c7675ded30701a068ce141f3e698a27aaa35c702d8e078b/msgpack-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:1a3effc392a57744e4681e55d05f97d5ee7b598747d718340a9b4b8a970c40e1", size = 72484, upload-time = "2026-06-11T04:15:52.289Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/1d948420fdaa24de4efdb8012a6a5bebe09c82ee002b8c2ca745e9917f1f/msgpack-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:56a318f7df6bec7b40928d6b0519961f20a510d8baabf6baa393a70444588f0a", size = 66657, upload-time = "2026-06-11T04:15:53.583Z" }, + { url = "https://files.pythonhosted.org/packages/39/16/1674faa1b7bddc19e79b465fd8e88e2cf4e3f7cae90723740701e8541068/msgpack-1.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:afa4a65ab2097795e771a74a3a81ea49534aaeba874eaf426a3332268e045ae6", size = 86093, upload-time = "2026-06-11T04:15:54.98Z" }, + { url = "https://files.pythonhosted.org/packages/dd/24/f241bcfdd9e96b2246289357c5a5e5a496189fd41c5844bee802c116aac7/msgpack-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:409550770632bb28daa70a11d0ed5763f7db38f40b06f7db9f11dd2794d01102", size = 86372, upload-time = "2026-06-11T04:15:56.381Z" }, + { url = "https://files.pythonhosted.org/packages/94/c9/57f8ab98a1b21808c27b6dd6029053e0a796ffbb9b371e460dbe997011a9/msgpack-1.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf47e3cd11ce044965a9736a322afdd390b31ed602d1c1b10211d1a841f1d587", size = 428207, upload-time = "2026-06-11T04:15:57.739Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/4fd4aa739f131ded751ca7167c8ee87d2aab32506ebbeea893b60b51d343/msgpack-1.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:204bc9f5d6e59c1718c0a4a84fc8ff71b5b4562faac257c1a68bca611ecf9b72", size = 426082, upload-time = "2026-06-11T04:15:59.356Z" }, + { url = "https://files.pythonhosted.org/packages/f9/00/db88e9a08fcd6513decaad06cbd5c168142bc3e662fb2f1aca3a563b7aa1/msgpack-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:610154307b27267266368bc1d1c7bb8aeb71da7be9356d403cb2442d9e6399f5", size = 378355, upload-time = "2026-06-11T04:16:00.916Z" }, + { url = "https://files.pythonhosted.org/packages/54/84/eee4dd703d7a600cf46159d621c070b0b9468cf3dbade4ea8272bf5232a4/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6799f157bb63e79f11e2e590cfdb28423fc18dd60c270c3914b5b4586ae36f7e", size = 410848, upload-time = "2026-06-11T04:16:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/0a/195e2c549fd4631eb7f157d016ff15a10c4c1cf82b6d0a9b1edaef5174b1/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:72bd844902cf0a5ac3af2ef742f253cd0b1e5bcd184f49b4fb9a6a1f7bf305e8", size = 376152, upload-time = "2026-06-11T04:16:04.041Z" }, + { url = "https://files.pythonhosted.org/packages/45/9b/bdd143fa79baec411dc658f5686fed680a18b36fcea5fccb6af1b8c7d832/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3c0bd450f78d0d81722c80da6cdbf674a856967870a9db2f6c4debc4d8b3c67c", size = 417061, upload-time = "2026-06-11T04:16:05.63Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ce/011ffcd8b919f55196ec53f12ae162e21c879d95afba226894314ff62c07/msgpack-1.2.0-cp314-cp314t-win32.whl", hash = "sha256:378caf74c4c718dfc17590ce68a6d710ed398ff6fcf08237de23b77755730b55", size = 70782, upload-time = "2026-06-11T04:16:07.105Z" }, + { url = "https://files.pythonhosted.org/packages/57/a8/9b8791ca96b1be6b9f659c718271e2cb7f99f73f58aad2dd0b30f750f6c0/msgpack-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:553b42598165c4dd3235994fd6e4b0dfb1ce5f3fd33d94ba9609442643015f38", size = 77899, upload-time = "2026-06-11T04:16:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/5b/04/3fa2dffb87bf598696b86bde7cd642d0a7590520c3fa24cd19611dfebeb7/msgpack-1.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2825bb1da548d214ab8a810906b7dd69a10f3838b615a2cc46e5172d3cb44f6e", size = 71004, upload-time = "2026-06-11T04:16:09.556Z" }, +] + +[[package]] +name = "narwhals" +version = "2.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/3c/c4ef2164a71c1a63d7f1ae411c4082c5fa872405106db60a4b7114989ad7/narwhals-2.22.1.tar.gz", hash = "sha256:d62920805a0a43b7ff8b54b0c0d3142d796f8a9301836ada37e573d6a33cbcd9", size = 647493, upload-time = "2026-06-05T12:34:34.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ca/36339329c4604adbcc99c899b7eb1ce1a555c499b6a6860757dc9bfed36d/narwhals-2.22.1-py3-none-any.whl", hash = "sha256:60567d774edf77db53906f89d9fbd164e66e56d66d388e1e6990f17ac33cfb53", size = 454815, upload-time = "2026-06-05T12:34:32.289Z" }, ] [[package]] @@ -2381,14 +2523,9 @@ name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.13'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } @@ -2422,9 +2559,9 @@ wheels = [ [[package]] name = "nuitka" -version = "4.1" +version = "4.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/db/b7a344ad688cd6d8547746869f904b105674ff529a24fa5a3d7bdd95560a/nuitka-4.1.tar.gz", hash = "sha256:99092d26f5f8d5264186924451f7df5872bf6a922297062ace2798ecec7cfa0f", size = 4543258, upload-time = "2026-05-11T07:16:35.483Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/2e/9ea398ca1a4fc458958fdf477ae18d3395bee8c9f8950ca6f0f039ea2585/nuitka-4.1.2.tar.gz", hash = "sha256:efc2359b171d7b63046ca8ec8dee57015c3466a9df74b68a049c2c1a7e93ecee", size = 4561050, upload-time = "2026-05-28T08:26:07.947Z" } [[package]] name = "numba" @@ -2505,12 +2642,7 @@ name = "numpy" version = "2.4.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.13'", ] sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } wheels = [ @@ -2627,7 +2759,7 @@ wheels = [ [[package]] name = "openai" -version = "2.36.0" +version = "2.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2639,9 +2771,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/a1/4d5e84cf51720fc1526cc49e10ac1961abcccb55b0efb3d970db1e9a2728/openai-2.36.0.tar.gz", hash = "sha256:139dea0edd2f1b30c33d46ae1a6929e03906254140318e4608e98fe8c566f2e7", size = 753003, upload-time = "2026-05-07T17:33:17.075Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/36/4c926a91554483977608951360c18c2e911592785eb87a6437813f6123f7/openai-2.41.1.tar.gz", hash = "sha256:23d617a0432457ad844973bee8f540be9da90894f7c5686852d2d365da058f57", size = 783584, upload-time = "2026-06-10T16:10:37.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/1c/5d43735b2553baae2a5e899dcbcd0670a86930d993184d72ca909bf11c9b/openai-2.36.0-py3-none-any.whl", hash = "sha256:143f6194b548dbc2c921af1f1b03b9f14c85fed8a75b5b516f5bcc11a2a50c63", size = 1302361, upload-time = "2026-05-07T17:33:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/20/74/925d7b3892927e9804aaf58d374a45dc28e4420ff90e992272b77286343e/openai-2.41.1-py3-none-any.whl", hash = "sha256:a939565f350cb7443cb843b801b88c716ac8024b492fb94ca269d5f6b1bbefd6", size = 1353380, upload-time = "2026-06-10T16:10:35.756Z" }, ] [[package]] @@ -2931,11 +3063,11 @@ wheels = [ [[package]] name = "pip" -version = "26.1.1" +version = "26.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/48/cb9b7a682f6fe01a4221e1728941dd4ac3cd9090a17db3779d6ff490b602/pip-26.1.1.tar.gz", hash = "sha256:d36762751d156a4ee895de8af39aa0abeeeb577f93a2eca6ab62467bbf0f8a78", size = 1840400, upload-time = "2026-05-04T19:02:21.248Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl", hash = "sha256:99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb", size = 1812777, upload-time = "2026-05-04T19:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, ] [[package]] @@ -2952,7 +3084,7 @@ wheels = [ [[package]] name = "pip-audit" -version = "2.10.0" +version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachecontrol", extra = ["filecache"] }, @@ -2966,9 +3098,9 @@ dependencies = [ { name = "tomli" }, { name = "tomli-w" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/89/0e999b413facab81c33d118f3ac3739fd02c0622ccf7c4e82e37cebd8447/pip_audit-2.10.0.tar.gz", hash = "sha256:427ea5bf61d1d06b98b1ae29b7feacc00288a2eced52c9c58ceed5253ef6c2a4", size = 53776, upload-time = "2025-12-01T23:42:40.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/a4/f21d5f0a0edabcbce31560b73c7c5a6f72ae87af4236fd1069c8f59a353d/pip_audit-2.10.1.tar.gz", hash = "sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc", size = 54275, upload-time = "2026-06-10T22:17:01.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/f3/4888f895c02afa085630a3a3329d1b18b998874642ad4c530e9a4d7851fe/pip_audit-2.10.0-py3-none-any.whl", hash = "sha256:16e02093872fac97580303f0848fa3ad64f7ecf600736ea7835a2b24de49613f", size = 61518, upload-time = "2025-12-01T23:42:39.193Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a7/b0c504148114047bd1bc9d97447453c6850ca176bb2f3c0038835994e8b7/pip_audit-2.10.1-py3-none-any.whl", hash = "sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a", size = 62023, upload-time = "2026-06-10T22:17:00.309Z" }, ] [[package]] @@ -2986,11 +3118,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.6" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] @@ -3061,17 +3193,17 @@ wheels = [ [[package]] name = "protobuf" -version = "7.34.1" +version = "7.35.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, - { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, - { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, - { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, - { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, - { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, - { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, ] [[package]] @@ -3331,14 +3463,14 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.12.1" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, ] [package.optional-dependencies] @@ -3355,7 +3487,7 @@ dependencies = [ { name = "llvmlite", marker = "python_full_version < '3.13'" }, { name = "numba", marker = "python_full_version < '3.13'" }, { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] @@ -3375,14 +3507,14 @@ wheels = [ [[package]] name = "pypdf" -version = "6.11.0" +version = "6.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/58/6dd97d78a4b17a7a6b9d1c6ad23895abc41f0fdc49c553cc05bdfdcc36d0/pypdf-6.11.0.tar.gz", hash = "sha256:062b51c81b0910e6d2755e99e1c5547a0a23b7d0a32322af66240d8edcfabe87", size = 6453975, upload-time = "2026-05-09T13:26:48.955Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/0a/48fe05c6bb3aa4bb4d2a4079a383d33c0dfec1edf613a642f07d8b8b5c2e/pypdf-6.13.2.tar.gz", hash = "sha256:5a96a17dbdfbf9c2ab24c0a13fa0aba182be22ba6f283098712c16fc242f509f", size = 6479250, upload-time = "2026-06-10T16:42:34.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/b1/68feb7eb3b99f0c020b414234825f4a5d70e0126c18d933770e8c93a35fc/pypdf-6.11.0-py3-none-any.whl", hash = "sha256:769394d5756d5b304c9b6bef88b54b1816b328e7e6fc9254e625529a15ed4ab8", size = 338819, upload-time = "2026-05-09T13:26:46.904Z" }, + { url = "https://files.pythonhosted.org/packages/cb/17/378943705992f74e451a06de3401ce68e3213763c81e44d0614559c45599/pypdf-6.13.2-py3-none-any.whl", hash = "sha256:6eeb9e57693f29d41bd01255d02660cbbb41fd7fc818a982677389a35e4f2083", size = 346555, upload-time = "2026-06-10T16:42:32.37Z" }, ] [[package]] @@ -3396,15 +3528,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.409" +version = "1.1.410" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/4e/3aa27f74211522dba7e9cbc3e74de779c6d4b654c54e50a4840623be8014/pyright-1.1.409.tar.gz", hash = "sha256:986ee05beca9e077c165758ad123667c679e050059a2546aa02473930394bc93", size = 4430434, upload-time = "2026-04-23T11:02:03.799Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/53/e4d8ea1391bd4355231be6f91bf239479aa0014260ed3fb5526eeb12a1f2/pyright-1.1.410.tar.gz", hash = "sha256:07a073b8ba6749826773c1269773efa11b93440d9a6aa60419d9a3172d6dc488", size = 4062013, upload-time = "2026-06-01T17:35:48.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/6b/330d8ebae582b30c2959a1ef4c3bc344ebde48c2ff0c3f113c4710735e11/pyright-1.1.409-py3-none-any.whl", hash = "sha256:aa3ea228cab90c845c7a60d28db7a844c04315356392aa09fafcee98c8c22fb3", size = 6438161, upload-time = "2026-04-23T11:02:01.309Z" }, + { url = "https://files.pythonhosted.org/packages/d7/33/288b5868fa00846dacf249633719d747893e54aebd196b9968ac1878a5d3/pyright-1.1.410-py3-none-any.whl", hash = "sha256:5e961bed37cacf96b3f7cd7b1da39b350a9239aa2e69138d0e88f728cfaf296c", size = 6082448, upload-time = "2026-06-01T17:35:46.387Z" }, ] [[package]] @@ -3453,15 +3585,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.3.1" +version = "1.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/60/e88788207d81e46362cfbef0d4aaf4c0f49efc3c12d4c3fa3f542c34ebec/python_discovery-1.3.1.tar.gz", hash = "sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6", size = 68011, upload-time = "2026-05-12T20:53:36.336Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl", hash = "sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c", size = 33185, upload-time = "2026-05-12T20:53:34.969Z" }, + { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" }, ] [[package]] @@ -3488,11 +3620,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.28" +version = "0.0.32" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" }, + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] [[package]] @@ -3506,24 +3638,27 @@ wheels = [ [[package]] name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, - { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, ] [[package]] @@ -3680,13 +3815,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/b5/363906b1064fc6fe611783a61764927bbd91919aaaabe8cba82151ca93ef/rapidfuzz-3.14.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:dfef96543ced67d9513a422755db422ae1dc34dade0a1485e0b43e7342ed3ebf", size = 1509889, upload-time = "2026-04-07T11:16:28.487Z" }, ] +[[package]] +name = "redis" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/ae/ed461cca5780b5fc8b9fe8ca0ed98d89508645fb9d880c24cc42c087678f/redis-8.0.0.tar.gz", hash = "sha256:a00c5355432051ac14e593b8b197fc76c887ee12d55a0984f69328a1115fdc49", size = 5101591, upload-time = "2026-05-28T12:45:13.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/e3/b519734372d305bd547534a9f32e4ce9f98552af753dce72cf3483a0ff0b/redis-8.0.0-py3-none-any.whl", hash = "sha256:c938c18338585009f0bc310f4c7e4e4b4d37639356c4ac072cedf3af570c8dc7", size = 499870, upload-time = "2026-05-28T12:45:11.697Z" }, +] + [[package]] name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, - { name = "rpds-py" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } @@ -3847,6 +3995,9 @@ wheels = [ name = "rpds-py" version = "0.30.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, @@ -3965,6 +4116,149 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] +[[package]] +name = "rpds-py" +version = "2026.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/a0/acf8b6fc20bfdcd3a45bd3f57680fb198e157b7e997b9123b10763798bd2/rpds_py-2026.5.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3397a5ed7174dc2786bb214030232fc36fe8e5584fec43a9952cc542b1a12036", size = 355609, upload-time = "2026-05-28T11:58:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/b6/95/f8203fd997484b1690a6869cd0e503b6c3c6be55b0ecc36d1a491fe742f0/rpds_py-2026.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc", size = 348460, upload-time = "2026-05-28T11:58:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/33/8c/b47326ad2f0be545a5e5c1a55937a12afaea7d392ba2837bb9680f57e6c9/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164", size = 381031, upload-time = "2026-05-28T11:58:53.775Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/e83bbd97ffac6f6389b605cd4e1c8ac5761dc7e977769c9255d8c5adb7bd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead", size = 387121, upload-time = "2026-05-28T11:58:55.243Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0e/d285d1bc8864245919c61e1ca82263e4a66d337759c3a4cef72766ff9afc/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece", size = 501026, upload-time = "2026-05-28T11:58:56.788Z" }, + { url = "https://files.pythonhosted.org/packages/86/06/ccb2109a1e543437b5e43816f2b43b9554cc6783145528a4e3711e05c011/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb", size = 391865, upload-time = "2026-05-28T11:58:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/3d/33/237173db1cfef10105b3839a24de00eb8d2a523711add4632447cdf0aedd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda", size = 378012, upload-time = "2026-05-28T11:58:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/97/64/1eae54e34d5161f9969295e80bd6b62a55f2b6ac5f2a5b60d02c2140e758/rpds_py-2026.5.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a", size = 391111, upload-time = "2026-05-28T11:59:01.104Z" }, + { url = "https://files.pythonhosted.org/packages/d8/34/5bb334a5a0f65d77869217c4654f34c78a7d11b93938a3c076a2edeafc52/rpds_py-2026.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe71bca7d547acb17027c7fd1624ff8aae623499c498d3e7011182c4de5c25e0", size = 409225, upload-time = "2026-05-28T11:59:02.433Z" }, + { url = "https://files.pythonhosted.org/packages/16/0f/007ec21283b5b040b4ec3bd95e0402591e22bfa7d5c93dfe01c465c2d2d7/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a", size = 556487, upload-time = "2026-05-28T11:59:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/ff/10/5437c94508169b6b22d8418fef7a66e9ffb5f3b9e9c94460f2eedafe06ff/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df1d2a1996755b24b9ecee92cb4d36c28f86f464a6a173349c26bab41e94b8c2", size = 620798, upload-time = "2026-05-28T11:59:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d5/9937dce4d6bda74157b954e7d1460db05a22f5929dccfeeba1ed27a93df0/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2", size = 584053, upload-time = "2026-05-28T11:59:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/750617dd0ae1752471bf43f9e41d263398fae7cde7849d23b8574a70e617/rpds_py-2026.5.1-cp311-cp311-win32.whl", hash = "sha256:3684a59b158a7683aaeb8e25352e9a9dd2122cec78f2d8530266e4f91b4c7b3f", size = 214390, upload-time = "2026-05-28T11:59:08.402Z" }, + { url = "https://files.pythonhosted.org/packages/3c/bb/3dcab0e1d9516303f2eb672a5d6f62eca5a69e2886301e9c8c54b520c39b/rpds_py-2026.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bd530e6a530bb3ea892f194fafa455f3516ac25ecf7143fd33c09be62b0470a", size = 231097, upload-time = "2026-05-28T11:59:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/49/d6/c6bbf5cb1cf12b9732df8074b57f6ef8341ba884c95d40632ae8bddb44e4/rpds_py-2026.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:0a5ae4dbe43c1076983b72616496919872ae7bbe7a1e21cc48336bc3154d130b", size = 226361, upload-time = "2026-05-28T11:59:11.079Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" }, + { url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" }, + { url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" }, + { url = "https://files.pythonhosted.org/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" }, + { url = "https://files.pythonhosted.org/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" }, + { url = "https://files.pythonhosted.org/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, + { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, + { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, + { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, + { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, + { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, + { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, + { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, + { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, + { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, + { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, + { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, + { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, + { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, + { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, + { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, + { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, + { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, + { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, + { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, + { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, + { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, + { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, + { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/42/56/3fe0fb34820ff667be791b3a3c22b85e8bcba54e9c832f47438c191fa7be/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:edf2765d84e42447f112ad877af8fe1db0089aaec5b28e88d6eab45e7fe99cea", size = 357151, upload-time = "2026-05-28T12:01:53.43Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/3eb9ccdb9f143b8c9b003978898cb497f942a324c077401e6b8834238e63/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb", size = 350195, upload-time = "2026-05-28T12:01:54.901Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/dbda232bc4f3ed732120692ab0d2c8402cb020516556d8bee622dcef2413/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df", size = 381850, upload-time = "2026-05-28T12:01:56.601Z" }, + { url = "https://files.pythonhosted.org/packages/40/30/32e769839a358f78810c234f160f2cc21d1e4e47e1c0e0e0d535be5a0219/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7", size = 387899, upload-time = "2026-05-28T12:01:58.212Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/ec84d243aadb3b34b71dd26a010d0930b2d284ff5fc9a69fec53810ee6fd/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc", size = 501618, upload-time = "2026-05-28T12:01:59.888Z" }, + { url = "https://files.pythonhosted.org/packages/74/25/b60e52686bbff777a64f9e4f4d3dd57980dc846913777177a2c92e4937aa/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162", size = 394003, upload-time = "2026-05-28T12:02:01.482Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c7/b3a6a588cc2219510ef3f42e207483a93950bedd1e3a0fd4015c95cff9e5/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251", size = 379778, upload-time = "2026-05-28T12:02:03.197Z" }, + { url = "https://files.pythonhosted.org/packages/31/00/c7dba3fc8a3da8cb3f6db1eb3386be4d79c2e97c6890d20eb9ac66ae8c43/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a", size = 392359, upload-time = "2026-05-28T12:02:04.817Z" }, + { url = "https://files.pythonhosted.org/packages/93/dd/472ba494c70753f93745992c99855bee0636daf74e6984e5e003f150316f/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4abbf391a70be864920858bf360f4fb380577c9a0f732438a1996726e2c195b", size = 412820, upload-time = "2026-05-28T12:02:06.401Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6f/93831a3bfe789542ed0c1d0d74b78b440f055d6dc3ea4640eba2d95e6e23/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34", size = 557243, upload-time = "2026-05-28T12:02:08.013Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ff/0b3d604614ffc77522c6b288fdbce68957eb583da1002aa65ba38ac0ee40/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:8213afbe8a3a906fb9acb2014423fe3359ee783d0bf90995f70623a3217bfa6c", size = 623541, upload-time = "2026-05-28T12:02:09.661Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ea/e7b0251441da9adfeaebcf29601d10f2a1455fcf0772fae9e7e19032bd96/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049", size = 586326, upload-time = "2026-05-28T12:02:11.47Z" }, +] + [[package]] name = "ruamel-yaml" version = "0.19.1" @@ -3976,47 +4270,48 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" }, - { url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" }, - { url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" }, - { url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" }, - { url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" }, - { url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" }, - { url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" }, - { url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" }, - { url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" }, - { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" }, +version = "0.15.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" }, + { url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" }, + { url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" }, + { url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" }, + { url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" }, + { url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" }, + { url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" }, + { url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" }, ] [[package]] name = "s3transfer" -version = "0.17.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/ec/7c692cde9125b77e84b307354d4fb705f98b8ccad59a036d5957ca75bfc3/s3transfer-0.17.0.tar.gz", hash = "sha256:9edeb6d1c3c2f89d6050348548834ad8289610d886e5bf7b7207728bd43ce33a", size = 155337, upload-time = "2026-04-29T22:07:36.33Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/1f/12417f7f493fc45e1f9fd5d4a9b6c125cf8d2cf3f8ddbdfab3e76406e9d6/s3transfer-0.18.0.tar.gz", hash = "sha256:3760b8b7ec1315da54048b2d626276732bee4300d054d492d4e1d43e20d4ecbd", size = 160560, upload-time = "2026-05-28T19:39:09.124Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/72/c6c32d2b657fa3dad1de340254e14390b1e334ce38268b7ad51abda3c8c2/s3transfer-0.17.0-py3-none-any.whl", hash = "sha256:ce3801712acf4ad3e89fb9990df97b4972e93f4b3b0004d214be5bce12814c20", size = 86811, upload-time = "2026-04-29T22:07:34.966Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a58fc997655386daa2e25784e30c288aa3e3819e401f77029ee4899fb55a/s3transfer-0.18.0-py3-none-any.whl", hash = "sha256:239c13b09e65ad0346e1be7348b8a202dcad44ac7ea7c6eb858fc881dce739b6", size = 88572, upload-time = "2026-05-28T19:39:07.999Z" }, ] [[package]] name = "safety" -version = "3.7.0" +version = "3.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, + { name = "certifi" }, { name = "click" }, { name = "dparse" }, { name = "filelock" }, @@ -4026,18 +4321,18 @@ dependencies = [ { name = "nltk" }, { name = "packaging" }, { name = "pydantic" }, - { name = "requests" }, { name = "ruamel-yaml" }, { name = "safety-schemas" }, { name = "tenacity" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomlkit" }, + { name = "truststore" }, { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/e8/1cfffa0d8836de8aa31f4fa7fdeb892c7cfa97cd555039ad5df71ce0e968/safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e", size = 330538, upload-time = "2025-11-06T20:10:15.067Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/7b/8e1d580c5178f0736b806b7199827e61e2a2569eec5b49ec75da6273bbdf/safety-3.8.1.tar.gz", hash = "sha256:e646123b976bbb6707cfaacae8c926e2f886b744a60e0f410e8610a3a4eaf7be", size = 412947, upload-time = "2026-05-29T15:09:33.355Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/55/c4b2058ca346e58124ba082a3596e30dc1f5793710f8173156c7c2d77048/safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf", size = 312436, upload-time = "2025-11-06T20:10:13.576Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f5/498db84333a644835e572c0d96cfa705bf9871f863289a7845082d54755e/safety-3.8.1-py3-none-any.whl", hash = "sha256:953c1c3c60c873f53a6cc250b2a9c4b38bb6ef45f0625990e43f20bff916c965", size = 340521, upload-time = "2026-05-29T15:09:31.622Z" }, ] [[package]] @@ -4105,7 +4400,7 @@ wheels = [ [[package]] name = "scikit-learn" -version = "1.8.0" +version = "1.9.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", @@ -4114,48 +4409,43 @@ resolution-markers = [ ] dependencies = [ { name = "joblib", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "narwhals", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "threadpoolctl", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, - { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, - { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, - { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, - { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, - { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, - { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, - { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, - { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, - { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, - { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, - { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, - { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, - { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, - { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, - { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, - { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, - { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, - { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, - { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, - { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, - { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, - { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, - { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, - { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, - { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, - { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, - { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, - { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/e844fd9586e66540a15b71924d17a6cbc1bb749e81ddd0a796bcdba4c055/scikit_learn-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b", size = 8789686, upload-time = "2026-06-02T11:53:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" }, + { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/be3d369f40d8178ba3bd86635d132e08cb5329b023e4669d9426d84bc007/scikit_learn-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759", size = 8272736, upload-time = "2026-06-02T11:53:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/37/79/a733f02dc2118da7e77a134b34f39f40201a353311b011d20859d2db3556/scikit_learn-1.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28", size = 7919564, upload-time = "2026-06-02T11:53:21.2Z" }, + { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, + { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/cf3310626b6d48d3e9be69a1223f9180360b5e6edb045f50fade723ce494/scikit_learn-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119", size = 8705188, upload-time = "2026-06-02T11:53:41.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, + { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, + { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" }, + { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/c9a35cf59b20a86fec24d306f1547b78dec194b08d367ce2a3e4854169d9/scikit_learn-1.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9656acd4e93f74e0b66c8a36c88830a99252dfa900044d36bc2212ae89a47162", size = 8713289, upload-time = "2026-06-02T11:53:58.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a7/552a7821597c632b907f7bfe8f36f9f572777af8ef8a48353041cf8e091a/scikit_learn-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:24360002ae845e7866522b0a5bbf690802e7bc388cac8663502e78aa98598aa2", size = 8245141, upload-time = "2026-06-02T11:54:01.694Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/f4a0c4fe9711154cddabf913471153af79056382ddc612cfe5ee0ff4b72e/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5162ad10a418c8a282dde04c9aa06965de3e9a65f33c1440c0ae69bb1a09d913", size = 8847671, upload-time = "2026-06-02T11:54:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/4d72d9e475ac83719160c662619e4bf7b95c19507cd582e7d0167a3c3dae/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fea2cc5677ab49d6f5bade978c866da44957b712d92e9635e8b4f723013c3cb", size = 9118104, upload-time = "2026-06-02T11:54:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d5/6a58eea2cb9abbb9b3f2bb8b2cfb3243d1152d69f442d256c7af71304769/scikit_learn-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:64fa347efc1c839c487433e40c5144d38c336e8a2b59c81aa8660373945c2673", size = 8290674, upload-time = "2026-06-02T11:54:10.087Z" }, + { url = "https://files.pythonhosted.org/packages/65/5b/d4c879cf358f1187141cf90ced473f087183489090244f50c124a2ee478b/scikit_learn-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:1b944b6db288f6b926e3650026ddafb988929de95d11fc2cc5fa117773c9ba42", size = 7978807, upload-time = "2026-06-02T11:54:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/8a/43/bfae3121ec67ae09150d453c442c7c1cc166e9aefe056e6ab3b7728a5cfc/scikit_learn-1.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4ccacf04ca5f4b492158a5f28afe0ace43f81b2571e4b9a66d34848b46128949", size = 9031941, upload-time = "2026-06-02T11:54:15.436Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/20a4546eb17f3b25d3c66df15810411c14ed5065bcfab50b53c96fb627b2/scikit_learn-1.9.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ee1a8db2c18c08e34c7412d4b10be1cac214cd4ea7dc9715a6a327eb49a37c96", size = 8613528, upload-time = "2026-06-02T11:54:18.842Z" }, + { url = "https://files.pythonhosted.org/packages/18/3c/e440e039bb82cd19004edaaad00acbde0fb9b461083c3ecf37941c557312/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b", size = 8855050, upload-time = "2026-06-02T11:54:21.699Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/b341b8dab5998da6270a3a42c2152c578501354d36f944b5856757035ef8/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bad8f8b9950321b54c965fdcbac6c6c55e79e16646b49977bcf3668d3870a1a", size = 9097190, upload-time = "2026-06-02T11:54:24.454Z" }, + { url = "https://files.pythonhosted.org/packages/fb/de/b650b4d69b84468cfa2e28a3ff7b8103743029e6446ce1a97fe060ef688c/scikit_learn-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:78fc56eafd4edb9575d2d8950d1dd152061abb573341a1cb7e099fc40f6c6666", size = 8963204, upload-time = "2026-06-02T11:54:27.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/ff83d76d7418112e5a61326443cdda87be3545dd8d6599c95b2481a4419e/scikit_learn-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa", size = 8222661, upload-time = "2026-06-02T11:54:30.192Z" }, ] [[package]] @@ -4298,7 +4588,8 @@ name = "seaborn" version = "0.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "matplotlib", marker = "python_full_version < '3.13'" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "matplotlib", version = "3.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, @@ -4367,11 +4658,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.3" +version = "2.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, ] [[package]] @@ -4389,15 +4680,15 @@ wheels = [ [[package]] name = "starlette" -version = "1.0.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] [[package]] @@ -4636,14 +4927,14 @@ wheels = [ [[package]] name = "tqdm" -version = "4.67.3" +version = "4.68.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/05/0d5260f1f1ca784f4a4a0def9cbe6affe587f5b4025328d446c3d67765f4/tqdm-4.68.2.tar.gz", hash = "sha256:89c230e8dbc67c7615c142487111222f878c77427ea09549960f62389e258add", size = 171923, upload-time = "2026-06-09T13:26:42.539Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, + { url = "https://files.pythonhosted.org/packages/eb/75/1a0392bcc21c44dcdf87b3cf2d137e7829be2c083a1e38d44efca3d57a16/tqdm-4.68.2-py3-none-any.whl", hash = "sha256:d4240441fb5353290b87d6a85968c9decc131a99b8c7faa28269d829de669ede", size = 78578, upload-time = "2026-06-09T13:26:40.731Z" }, ] [[package]] @@ -5056,18 +5347,18 @@ wheels = [ [[package]] name = "tree-sitter-swift" -version = "0.7.2" +version = "0.7.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/45/6986ace9ad2eb7a111b7c47c8900192bc4d6c9f3db236fde873b7f8579c3/tree_sitter_swift-0.7.2.tar.gz", hash = "sha256:67b9a3ba5ab8fff2c082a2c0c33c8b5a66539f8bfa5058385688b1aefc11cead", size = 926779, upload-time = "2026-05-04T05:05:13.461Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/aa/8e7b789bb74ad7b9efb784bfb7d42bbcf064288d7716a72b68211ac6c3d4/tree_sitter_swift-0.7.3.tar.gz", hash = "sha256:a87f1dba3050a346ee3442aad8d727afd74555dea258e31c71c7934d8c04af9b", size = 1015814, upload-time = "2026-06-01T00:42:20.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/7f/98abba4def5dca30ece6e3cd9fb09f0cddbdc250fd2d050d1cfdbe0c8924/tree_sitter_swift-0.7.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:4664a5cbf20f0090ea2de540abc4f3392479a89db516f9774a62885c1b61aac7", size = 330332, upload-time = "2026-05-04T05:05:03.176Z" }, - { url = "https://files.pythonhosted.org/packages/dd/dd/aee99d2ccf0deb48e84656fefdecf059392a6778d3f050bf33cfa1d6074c/tree_sitter_swift-0.7.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d5791dbec5e4070accc0e06d231e18879d67edab98369685a81a1f77e024727", size = 352232, upload-time = "2026-05-04T05:05:04.493Z" }, - { url = "https://files.pythonhosted.org/packages/c9/74/0af5181a67c71f09af7a9f7942ba8f65e22a4f4d6eed426e6daf6253d3a6/tree_sitter_swift-0.7.2-cp38-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:600053b3ed763beaa5156ba1d70b22602ed88a6cff6cf3aab238133983426f9e", size = 358235, upload-time = "2026-05-04T05:05:05.777Z" }, - { url = "https://files.pythonhosted.org/packages/34/04/e6ded10edc9ece2a5812058dace35bbae03685547d4bee03af843b7a9ca5/tree_sitter_swift-0.7.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c8398f0b105293bbae375c7701256772b90996044f822e8e590297cc671e6e4", size = 354699, upload-time = "2026-05-04T05:05:06.917Z" }, - { url = "https://files.pythonhosted.org/packages/8f/56/befd27fac44be001e0489cdeed8c5837ebba4e1a92d2155460f5a53c5fe1/tree_sitter_swift-0.7.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cfbd96472e4841dbacf903088044f4a6a0fb4fa5ef7084a5bf55a804fefcc013", size = 353478, upload-time = "2026-05-04T05:05:08.524Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fb/9acab9dd78a2fcbd04c90a42bd8f313d9ae719f4e3388cd1345d03bbe0de/tree_sitter_swift-0.7.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e4de7c8a789c6fe01e0e0ba2a2792e9d4db905eb146ed9a321502a848826ba84", size = 356772, upload-time = "2026-05-04T05:05:09.612Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/5eb7a57346a287fa9bd7d5757a9fc1cbaef4dc043093a565e91384a7df18/tree_sitter_swift-0.7.2-cp38-abi3-win_amd64.whl", hash = "sha256:dec5aa6bc475ccd41685ce88dfde5894077bed6123b85e89e2c027f5ab6ab09e", size = 337169, upload-time = "2026-05-04T05:05:11.138Z" }, - { url = "https://files.pythonhosted.org/packages/7d/00/43b80f23c282cd0391442c1e3e5d9e6fb8c3fd62add900d6879522dc81de/tree_sitter_swift-0.7.2-cp38-abi3-win_arm64.whl", hash = "sha256:c7d11ca989e1930a55a79bbea5964fa1b121d947fa25ec7c068364383c85e6c3", size = 333364, upload-time = "2026-05-04T05:05:12.458Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9d/df190b08548dcfa67790d3197442989b3dd5e46d31ee61a1b9ecea35d57b/tree_sitter_swift-0.7.3-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2531ec866c22ea52384e2786e07f3b2bb396c6446428a2df02cc74af3f7e6b6a", size = 357955, upload-time = "2026-06-01T00:42:10.954Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/84e2bc7826eb9007c531f47e5557461c5a48fd14bd3ea82424afa3d06b5f/tree_sitter_swift-0.7.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ee627e027d0868c552beca13dcdfa9944662b126f642464c5038ee3204e68340", size = 381009, upload-time = "2026-06-01T00:42:12.182Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/55f6cc9aad9079facf166d616472fd8e05007cbee9c62b749e153bf0521d/tree_sitter_swift-0.7.3-cp38-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f38feeb4f7350c8b30d567a0dc08bf1eeaa67c241b6888d72a45a8b1a4aa7187", size = 386994, upload-time = "2026-06-01T00:42:13.609Z" }, + { url = "https://files.pythonhosted.org/packages/ff/38/0b7c4d195d03396c19a7968a13342c89cb8322d97c4882bb7c4240adf419/tree_sitter_swift-0.7.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eee02fecb60a07267edd123148c583d6ec9efc5d7fcb25e53da4e56869fd4cf3", size = 381113, upload-time = "2026-06-01T00:42:14.776Z" }, + { url = "https://files.pythonhosted.org/packages/81/34/48014e4cee1e2cf194675beeb435612a781f5cfa3c6f0e14b023b70c5cd7/tree_sitter_swift-0.7.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f30c30831f090ebe245f54ddcd280d2c5f7020ba17d6bbec1662bbfae140c467", size = 380282, upload-time = "2026-06-01T00:42:15.818Z" }, + { url = "https://files.pythonhosted.org/packages/89/1c/7ed9e76f14918106a27c548efc64f123af4b8e6424fcae13481683bb09a4/tree_sitter_swift-0.7.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01c1e812289a2f7f01f63627a5d94a0b57d69332e8b52624becfe79ee8061651", size = 385590, upload-time = "2026-06-01T00:42:16.92Z" }, + { url = "https://files.pythonhosted.org/packages/6b/bb/e4e12fa0523c1acb2f9c4cebc454cd5415e94c915ad7f0b4b151ad13bc30/tree_sitter_swift-0.7.3-cp38-abi3-win_amd64.whl", hash = "sha256:4b1de6122cbd82b2cea6d3a295f9f5f9297601b829061119e161da17a7ba7d17", size = 365047, upload-time = "2026-06-01T00:42:18.02Z" }, + { url = "https://files.pythonhosted.org/packages/70/7b/faf0fa8a99a217952b57aa43ed1b85ede798b3e8af51344cb5234766f718/tree_sitter_swift-0.7.3-cp38-abi3-win_arm64.whl", hash = "sha256:af44acc50d16f284abb607ae0cf7f81011d5566283d6c62a045a549a9331a653", size = 359248, upload-time = "2026-06-01T00:42:19.135Z" }, ] [[package]] @@ -5115,6 +5406,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/8d/c0a481cc7bba9d39c533dd3098463854b5d3c4e6134496d9d83cd1331e51/tree_sitter_zig-1.1.2-cp39-abi3-win_arm64.whl", hash = "sha256:88152ebeaeca1431a6fc943a8b391fee6f6a8058f17435015135157735061ddf", size = 63219, upload-time = "2024-12-22T01:27:38.348Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "typer" version = "0.25.1" @@ -5169,7 +5469,7 @@ dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "pynndescent", marker = "python_full_version < '3.13'" }, { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, + { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, { name = "tqdm", marker = "python_full_version < '3.13'" }, @@ -5190,21 +5490,21 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.47.0" +version = "0.49.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, - { name = "h11", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "click", marker = "python_full_version < '3.11' or python_full_version >= '3.13' or sys_platform != 'emscripten'" }, + { name = "h11", marker = "python_full_version < '3.11' or python_full_version >= '3.13' or sys_platform != 'emscripten'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, ] [[package]] name = "virtualenv" -version = "21.3.3" +version = "21.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -5213,9 +5513,9 @@ dependencies = [ { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/50/7564c805bb8966d9771caaba8a143fa5e57c848ce4e7fdf2d55a1feb2ead/virtualenv-21.4.3.tar.gz", hash = "sha256:938ff0fd3f4e0f0d3a025f67a3d2f25e3c3aabbcd5857ea6170619138d72d141", size = 7644454, upload-time = "2026-06-11T16:47:04.843Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl", hash = "sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3", size = 7594554, upload-time = "2026-05-13T18:01:27.815Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8d/84b0d07c6b5f685f85ddf6c87a59d3a8a895a3dfd89e759666fabe951b94/virtualenv-21.4.3-py3-none-any.whl", hash = "sha256:75f4127d4067397c64f38579ce918fec6bf9ca2cd4f48685e82952cc3c035840", size = 7625544, upload-time = "2026-06-11T16:47:01.78Z" }, ] [[package]] @@ -5264,104 +5564,104 @@ wheels = [ [[package]] name = "wrapt" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/d2/387594fb592d027366645f3d7cc9b4d7ca7be93845fbaba6d835a912ef3c/wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c", size = 60669, upload-time = "2026-03-06T02:52:40.671Z" }, - { url = "https://files.pythonhosted.org/packages/c9/18/3f373935bc5509e7ac444c8026a56762e50c1183e7061797437ca96c12ce/wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f", size = 61603, upload-time = "2026-03-06T02:54:21.032Z" }, - { url = "https://files.pythonhosted.org/packages/c2/7a/32758ca2853b07a887a4574b74e28843919103194bb47001a304e24af62f/wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb", size = 113632, upload-time = "2026-03-06T02:53:54.121Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d5/eeaa38f670d462e97d978b3b0d9ce06d5b91e54bebac6fbed867809216e7/wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e", size = 115644, upload-time = "2026-03-06T02:54:53.33Z" }, - { url = "https://files.pythonhosted.org/packages/e3/09/2a41506cb17affb0bdf9d5e2129c8c19e192b388c4c01d05e1b14db23c00/wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba", size = 112016, upload-time = "2026-03-06T02:54:43.274Z" }, - { url = "https://files.pythonhosted.org/packages/64/15/0e6c3f5e87caadc43db279724ee36979246d5194fa32fed489c73643ba59/wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f", size = 114823, upload-time = "2026-03-06T02:54:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/56/b2/0ad17c8248f4e57bedf44938c26ec3ee194715f812d2dbbd9d7ff4be6c06/wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394", size = 111244, upload-time = "2026-03-06T02:54:02.149Z" }, - { url = "https://files.pythonhosted.org/packages/ff/04/bcdba98c26f2c6522c7c09a726d5d9229120163493620205b2f76bd13c01/wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45", size = 113307, upload-time = "2026-03-06T02:54:12.428Z" }, - { url = "https://files.pythonhosted.org/packages/0e/1b/5e2883c6bc14143924e465a6fc5a92d09eeabe35310842a481fb0581f832/wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d", size = 57986, upload-time = "2026-03-06T02:54:26.823Z" }, - { url = "https://files.pythonhosted.org/packages/42/5a/4efc997bccadd3af5749c250b49412793bc41e13a83a486b2b54a33e240c/wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71", size = 60336, upload-time = "2026-03-06T02:54:18Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f5/a2bb833e20181b937e87c242645ed5d5aa9c373006b0467bfe1a35c727d0/wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc", size = 58757, upload-time = "2026-03-06T02:53:51.545Z" }, - { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" }, - { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" }, - { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" }, - { url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099, upload-time = "2026-03-06T02:54:56.74Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457, upload-time = "2026-03-06T02:53:52.842Z" }, - { url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351, upload-time = "2026-03-06T02:53:32.684Z" }, - { url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748, upload-time = "2026-03-06T02:53:08.455Z" }, - { url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783, upload-time = "2026-03-06T02:53:02.02Z" }, - { url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977, upload-time = "2026-03-06T02:53:27.844Z" }, - { url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336, upload-time = "2026-03-06T02:54:28.104Z" }, - { url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756, upload-time = "2026-03-06T02:53:16.319Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, - { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, - { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, - { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, - { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, - { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, - { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, - { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, - { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, - { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, - { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, - { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, - { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, - { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, - { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, - { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, - { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, - { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, - { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, - { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, - { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, - { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, - { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, - { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, - { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, - { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, - { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, - { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, - { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, - { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, - { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, - { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, - { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, - { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, - { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, - { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, - { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620, upload-time = "2026-05-22T14:49:43.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/8b/84bc1ea68b620fe0e2696a8cff07e82f4b962d952ab14efee8955997bb70/wrapt-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0f68f478004475d97906686e702ddbddeaf717c0b68ad2794384308f2dc713ae", size = 80093, upload-time = "2026-05-22T14:47:27.074Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/64ec81194a0bc708d9720174c998c8a32116e82b5b32c04e20a7fe01176c/wrapt-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e422b2d647a65d6b080cad5accd09055d3809bdff00c76fba8dca00ca935572a", size = 81183, upload-time = "2026-05-22T14:47:29.062Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/3d186944aae923631d1def58f4c4ff8f0b6309906afc0b6978de3e69b3e0/wrapt-2.2.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:036dfb40128819a751c6f451c6b9c10172c49e4c401aebcdb8ecf2aec1683598", size = 152494, upload-time = "2026-05-22T14:47:30.583Z" }, + { url = "https://files.pythonhosted.org/packages/01/d1/6b3d0ea995b867d2862aad5619bd5e17de09a9d64a821f46832dcd272d40/wrapt-2.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09ac16c081bebfd15d8e4dfa5bdc805990bbd52249ecff22530da7a129d6120b", size = 154310, upload-time = "2026-05-22T14:47:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4b/37ecb90a8c3753e580327fb40731a984b754e3df65d2ef932bf359fe4adc/wrapt-2.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07be671fa8875971222b0ba9059ed8b4dc738631122feba17c93aa36b4213e9a", size = 149002, upload-time = "2026-05-22T14:47:34.021Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/918884d9dfa84d0d135b42a51c00910f5c5447fe7a5e211a8e16ac324dd4/wrapt-2.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93fc2bf40cd7f4a0256010dce073d44eeb4a351b9bca94d0477ce2b6e62532b3", size = 153185, upload-time = "2026-05-22T14:47:35.722Z" }, + { url = "https://files.pythonhosted.org/packages/4c/00/382299d8ced610b29b59b099a89eda821e8c489aa152b7183748ac83f32a/wrapt-2.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ba519b2d765df9871a25879e6f7fa78948ea59a2a31f9c1a257e34b651994afc", size = 148040, upload-time = "2026-05-22T14:47:37.052Z" }, + { url = "https://files.pythonhosted.org/packages/6c/46/62a79b79e35bbebb1207ca5d15b81192f37f20cc5659cf4e3ce955b7fcc8/wrapt-2.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9011395be8db1827d106c6449b4bb6dd17e331ff6ec521f227e4588f1c78e46f", size = 151773, upload-time = "2026-05-22T14:47:38.713Z" }, + { url = "https://files.pythonhosted.org/packages/a1/db/95c152151d206d4b430516c89725306e92484072f38e65492afde63f6d19/wrapt-2.2.1-cp310-cp310-win32.whl", hash = "sha256:a8f7176b83664af44567e9cc06e0d3827823fcc1a5e52307ebb8ac3aa95860b9", size = 77393, upload-time = "2026-05-22T14:47:40.061Z" }, + { url = "https://files.pythonhosted.org/packages/13/d3/882d50452c6fbd13f24fe5d2644b97cdad2565a7e1522cbb6312de8a52cf/wrapt-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:d7f513d3185e6fec82d0c3518f2e6365d8b4e49f5f45f29640d5162d56a23b54", size = 80350, upload-time = "2026-05-22T14:47:41.194Z" }, + { url = "https://files.pythonhosted.org/packages/58/0f/148376523b4e370692286a9ba14d5715cf3c5b86da3bd3630926367b6b73/wrapt-2.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:44255c84bc57554fed822e83e70036b51afa9edb56fc7ca56c54410ece7898c9", size = 79149, upload-time = "2026-05-22T14:47:42.835Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ac/4370bde262c0e633e6c4f0e56d55095710024cf9a5cecc20c59a10de483c/wrapt-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd57607acc85678925940bd5df0385ff8332083a32fa8d7a43f8767f4997263c", size = 80321, upload-time = "2026-05-22T14:47:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/eb/79/b8ff3a61e71babf58a8cf4c0d63358e8bad383e15bf7f35e62d2f6b6e4a4/wrapt-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ae574d65c9fa8e86f64f6a7c2668f9fcd507b183e0e577619f504b883cb0a6c", size = 81216, upload-time = "2026-05-22T14:47:45.243Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fd/c0cac1f77c9c4f6fe58a920ca632ce379bb8be928720e11e8d73de28a5e9/wrapt-2.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a04c28c10ba7fd12842b109d2edb0678872a2fe65277ca4ff06a0d61edee245", size = 159208, upload-time = "2026-05-22T14:47:47.176Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4f/744132a7b2fbefa6b81118ec5942eca5fc2e9a129f9055a0c5e46885a549/wrapt-2.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e2f02472a1cbbf3884b365714a810b5947134a95ad6952b554cb8cce9d492b0", size = 160322, upload-time = "2026-05-22T14:47:49.04Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/b7cd9a22a06cf93e6482904ee6afc956248983553593fd1009296d1b3b31/wrapt-2.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac2745950b2bff80219c15ebf2fa9d8427eba7e249739f97e55c9d169e47e9e1", size = 153243, upload-time = "2026-05-22T14:47:50.386Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4a/eb79423192015f46f0db2872e7e04a3dde8d359b83411e8959e7c9287eaa/wrapt-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67a97e5b6c457f0cd3cfc19ebb2d84463e60c3ece754cc831e4281a3ca29bb18", size = 159231, upload-time = "2026-05-22T14:47:51.753Z" }, + { url = "https://files.pythonhosted.org/packages/ec/dc/435015b58ce33c6fc4104158fa91ddb0e809ab03a5751fb7465d1d461456/wrapt-2.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c803a3d331796255af51ba2c79ed0ac8275865b516c09e61f248d1e7aff31ce9", size = 152351, upload-time = "2026-05-22T14:47:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/77/ac/5d203f98df8fd136b95c5227139aea02d34505e18baf812d0c005df61963/wrapt-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9b984d1eb252145d6302c1dbd5e87fc6d404d45531447c84eadec04bf1fcb027", size = 158347, upload-time = "2026-05-22T14:47:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/a92427dbdc74e54c1674abbed27e61b2cb5e7a94441b8c1270c70671d928/wrapt-2.2.1-cp311-cp311-win32.whl", hash = "sha256:8a983a603a18c8708f024f7f6991b2e66159219abbf894634c5056243c55f3cd", size = 77562, upload-time = "2026-05-22T14:47:56.275Z" }, + { url = "https://files.pythonhosted.org/packages/c8/56/987b9c13b3e1c1a3c6de71284076f996b79caec90e75a87c044a40c23db9/wrapt-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:9c210a6994b21aa9b29e81c8d11560e8fdab54c117e9cff37870d0a27bde1343", size = 80616, upload-time = "2026-05-22T14:47:57.854Z" }, + { url = "https://files.pythonhosted.org/packages/7e/25/d01f560888d99d94a959c85533de349ce68d71ace3f2591d6ea8f632cfed/wrapt-2.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:401229e9d63ca09f9b8891ecf83798d26c11bbb445d11ed9f1836b6d4585b38a", size = 79025, upload-time = "2026-05-22T14:47:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/bfae7b9401583b6d05938cd16dedc43857d96da2f8a3d50d78cc515bf6ff/wrapt-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ffad790d9d11d8ecf9f17c4bb671a5b4089e4d8b575c46c5129597f41f836b0", size = 81021, upload-time = "2026-05-22T14:48:00.313Z" }, + { url = "https://files.pythonhosted.org/packages/26/58/80f6a6599f933f4caecc1cb3ee88a04faf81e8b9bddbd6109c688dd63e0f/wrapt-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:628f5220c7a904d5fc78f7075c8d7871433eb6d035c94728a22fdf85f193d2a8", size = 81692, upload-time = "2026-05-22T14:48:01.49Z" }, + { url = "https://files.pythonhosted.org/packages/17/93/fb357cc7847c58a8ae790be718903afa81a28d23e642c843dc4129e8a0b2/wrapt-2.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:61acce4257a9883669703c525447c5b4c392edf0f987ae77ec32668440158f0e", size = 169364, upload-time = "2026-05-22T14:48:02.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0b/76b601ee309a8bd556af0eecb184394c20b3c49aa9c8e085aa1ffacc2568/wrapt-2.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727ab4244622cd6ad2390f322642090c877d2e83a608d2653a7643ae5368d926", size = 171079, upload-time = "2026-05-22T14:48:04.22Z" }, + { url = "https://files.pythonhosted.org/packages/cd/87/ee3f32d5658e3e26d3e0e457922b47a36dd3bfbdfee7f97bb3e802344a66/wrapt-2.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03df9ebed4c73ab93fa8c07e3d41d818dfca1852b15731a3de59457b27814624", size = 160205, upload-time = "2026-05-22T14:48:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/ae2fd64277a67f5d7bffcf2d05eea1e476263fb2a072baf0b0129ab85984/wrapt-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9ff006f420b2ec8296aa56ade43ea7da3e997e85769f0aafc5e0661aacb710", size = 168922, upload-time = "2026-05-22T14:48:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f3/2d541a060c5bbafb9400bca4917e4d78bfd1f239f404782c86831a8f6b29/wrapt-2.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:844c858fc3bb7eacc0ba8efa904935d16aac6a4470948ad1e7e55c9f5a2a665f", size = 158388, upload-time = "2026-05-22T14:48:08.629Z" }, + { url = "https://files.pythonhosted.org/packages/1d/68/8d92c8800c57e93cb116ae9e9d6cbafc34fade5ee9f9107b6f203fb4dc35/wrapt-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87bacdaf225117a342a20d9c03438d701c02112f6e3f351ce9b7f32354f14797", size = 167682, upload-time = "2026-05-22T14:48:10.042Z" }, + { url = "https://files.pythonhosted.org/packages/30/72/83ea3790ea352439442349388e29ff07b76e0686265f9088bbb505d1608d/wrapt-2.2.1-cp312-cp312-win32.whl", hash = "sha256:2f8c90c8afde51969487be4e1343ae049b268854877d415c2510baf833775052", size = 77857, upload-time = "2026-05-22T14:48:11.782Z" }, + { url = "https://files.pythonhosted.org/packages/ef/cb/99450668dd3502d62a54a1c8aa56e44f34cb8c1261b381cfe2e7926c3b75/wrapt-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ce32763ac31ce94fe9aada947e479b1975012bff166da409b4b9e4e376cf7e5", size = 80825, upload-time = "2026-05-22T14:48:13.046Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/87512881be64e743f9ee4c66f4cbe8e884974bef2a5989af71f999653ac7/wrapt-2.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d1b4d0e0c2119587a31f5c029abd547e0c81d93b89d394566fe1588659eb579", size = 79087, upload-time = "2026-05-22T14:48:14.323Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/a1b08f8f4fac8cbb156fa51cf64ee2c7f7f74f9875ba3cf70b3c58368694/wrapt-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2beb1c7cab10603aecdc42f8edd6ff013f9a32e4543474e38e6b77ce9975aeb", size = 80831, upload-time = "2026-05-22T14:48:15.598Z" }, + { url = "https://files.pythonhosted.org/packages/54/ce/57890814991446a845e09b3445ce8b694f27eb0577004f2c2a36a9772ed4/wrapt-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0cb7e4dd71f4c32e5e84843cd3c4cd65dda034314004bbe1d7f99af2426ab80", size = 81375, upload-time = "2026-05-22T14:48:17.071Z" }, + { url = "https://files.pythonhosted.org/packages/38/65/08d7a6c76ac4493bdb668205ee9c1de1bd5daca61717c3e9aa49b4c01499/wrapt-2.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95821352042722cd9f1108874579a47989d0a7e12a37d87d2fc4af20fd99ab8a", size = 167417, upload-time = "2026-05-22T14:48:18.303Z" }, + { url = "https://files.pythonhosted.org/packages/62/ce/f1ccbee7a1bfe5cdc6b3da6bab4b45713d628b9294da32a39f563d648140/wrapt-2.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abd621552ede77c4c69be7fac44ba911225b0c812b6ba604e5964cf98085b474", size = 166948, upload-time = "2026-05-22T14:48:19.768Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/f85d48d1cd4869aee6704028d257d740a47c1c467b457ce396b4b5b55d07/wrapt-2.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e3677c7146ce694874941ba82b57092cc4875445aadf29d72807351023105143", size = 158148, upload-time = "2026-05-22T14:48:21.96Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5c/93939ad11d4a12358ab1aab219a2ef5efa5612e0db6b9fc65af8af1a891b/wrapt-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9a5934eaea872e17936b5f45501eba5ab0bce9a74122e172b663d7c28c459c4a", size = 165905, upload-time = "2026-05-22T14:48:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/b8c2aa89862ff58605934d7abf4b70e6a5a1c33df96656f49035ccdf1c8a/wrapt-2.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f5b9daf6b629fce418e0cc3dd0436eac045188fa35deadb7a7f3941d5b8203f9", size = 156712, upload-time = "2026-05-22T14:48:24.767Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/bf00a7b02239c12bb02ddcc3c0b971bfcc36e578c5a44f1ccfef5b458545/wrapt-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f53ac9f3ef573326d009ed809beff4efcac6451931c2b8132586da4b9e53ff31", size = 166560, upload-time = "2026-05-22T14:48:26.83Z" }, + { url = "https://files.pythonhosted.org/packages/fe/93/6390ca9c5b787683cef588d04f57c8d41b9a2323b5597a65f18638c90ef2/wrapt-2.2.1-cp313-cp313-win32.whl", hash = "sha256:1ffa9cfd4bdb581539951b14ae661ff20ed0c3599b3e911a131ee0ec5ac11337", size = 77817, upload-time = "2026-05-22T14:48:28.221Z" }, + { url = "https://files.pythonhosted.org/packages/97/73/ce10f0e71c0cfaa1a65faadb8efd4852028b3bb9ba28932b8889df769d38/wrapt-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:368eac1e20fd0bb03dd3cc42bf9887154c3861b60989389ccb5fac032617d215", size = 80736, upload-time = "2026-05-22T14:48:30.139Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4c/89f4a6818fafbbd840330e4fa3873073e1bfc166133a64cac7f8fde7a5e3/wrapt-2.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:c754dafdf5aaf0b401b644a90a30046929a0dd1a536e0ff0ec959a59155d9c7f", size = 79099, upload-time = "2026-05-22T14:48:31.405Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f2/9a8741c46f8c208ac0a45b25ba170bcb4fb72a2781d5fb97dbd7b6be73cb/wrapt-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ed928d0fda15fc0adc8d13305c8b3c0f2fba5b0669950c9e6d019d9162a3b3e8", size = 82802, upload-time = "2026-05-22T14:48:33.307Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0d/e9c855716a3705eef1416456bdf062b60620726fdc59428ff670fc3c60dc/wrapt-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fafb4e739e43544d12cb4abd1605fd4683b6ca6a9ad682b7fd8f4d21973eafa8", size = 83329, upload-time = "2026-05-22T14:48:34.593Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d6/a88f1c13112b7831adac75cea65d8310e0d696d570c8961844c90a57b865/wrapt-2.2.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:74d6a0c31472fe5d814917266b9f46495d7c61ed890af08b468acea92fb89a8d", size = 202937, upload-time = "2026-05-22T14:48:35.859Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/e29d54aef06a4d898a5b8a25589a0b3769bde454f922fad8f6f89fbfb650/wrapt-2.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab5be648d5a0b86b7438864f8df3c705a65cef35a2fd3e5561e3e203167e0f27", size = 209997, upload-time = "2026-05-22T14:48:38.153Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/e4454263516cf0e12640912fbca9a83654e424f0a6ddb79f5cd7ce14bf33/wrapt-2.2.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d8f204c8e3a8bf9ece17e0a83d137fd807440977f8a5e762d59306795011440", size = 194856, upload-time = "2026-05-22T14:48:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/de/d0/fe0ee202286afdf4a7f77dd29f195703145764d572aec209c5086e57d924/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d047f6498c973874ba08ac3f97c69a2c4b2211c8de6f4c205f75cb1c9522596e", size = 205654, upload-time = "2026-05-22T14:48:43.456Z" }, + { url = "https://files.pythonhosted.org/packages/23/b6/87d860dfc6460c246af70b1fd5c8b76df77571b42a493459423ded94fd7d/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7a4fdb9326aab4a5a477a1640e5ad786a8495901009d7e7b038371edd23a9d2b", size = 192206, upload-time = "2026-05-22T14:48:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/df/46/3eea8cde077d985f239a38c0257087b8064fd9ee9b1a99e282d2c86da4ef/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8cc5094b08abeae52da9c73c8a32003623be691a5193df2f4e3eac3d557c394", size = 198428, upload-time = "2026-05-22T14:48:46.319Z" }, + { url = "https://files.pythonhosted.org/packages/18/dc/b927ee9c7fc67adc3a5658f246a0d275425eb840ba36e7b702e70f18bde8/wrapt-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:9907a4402ab6db12b7077a0ea5d7a4d028ecb22c8eee2b53527080d347cd1562", size = 79448, upload-time = "2026-05-22T14:48:47.901Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b3/fd30b473fe498c70e6b9a5f328b8d3fbaf1b8c3c481465f59724bba8eb70/wrapt-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5590d63f5243251641cf543009b4c9314a79d0598fdb8a8e4cfc918494536c53", size = 83021, upload-time = "2026-05-22T14:48:49.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/96c39153a8737a6e9aa85adef254ac4195bea3f2d24efc60472ccc3c9e2e/wrapt-2.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c318a64b53d97b841d7b5e637517e50a27be64bc695128422953d4b21710954e", size = 80295, upload-time = "2026-05-22T14:48:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a3/11d7f34ebbf3231bc907a3e6d5ee051b14d034c1bc7b65a97d5cc00516df/wrapt-2.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f56a647e4eaf5f0ca40330fb070f566bdf9f7b0db89a1af20d71c28dcd7a0ab", size = 80879, upload-time = "2026-05-22T14:48:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/b74cfd984cef560b900fb1a727af20352d89e1f06bf2e1114dd3f00f5f5a/wrapt-2.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:64b7deeda4b70408e382328d8bbe52a256fe9bc63ae3db86d804608367e5422c", size = 81462, upload-time = "2026-05-22T14:48:53.18Z" }, + { url = "https://files.pythonhosted.org/packages/15/a3/7c8f704b8dc07dfe0a5d01c2edbfd88317aa8e5e3fa7c743eb7a085ae767/wrapt-2.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9cf53ba90717db2e292401de290776c498d4bbfb0d4a559ca2895db8b9dcb5c", size = 167251, upload-time = "2026-05-22T14:48:54.562Z" }, + { url = "https://files.pythonhosted.org/packages/80/85/a34d1888d97247da6c2ff6118c3a721c73ed8cc4dd198c00208bb73b6f80/wrapt-2.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf3638274ab9d9b724c9baa0b4c04e132cd6faefb78b4dd3dd1a02a4bdaad41e", size = 166316, upload-time = "2026-05-22T14:48:56.065Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d7/72ffaeb01eebc704afe3fb99e840480f4bda45f0fa66e3381b6a39251c8f/wrapt-2.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aed9658797d0b45d6c49adcfc6b41f66e6f2d0c6de3ec79e16cf4b1855df240f", size = 157952, upload-time = "2026-05-22T14:48:57.924Z" }, + { url = "https://files.pythonhosted.org/packages/24/5b/36f5d6b024e4edfdd90b140742d11ebcf7836daf5c9daf326c55c24db412/wrapt-2.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d676ee388bc42a04d56dd7deb5605244dac2e35cc2fadbb43c9fa25bbd93508", size = 166130, upload-time = "2026-05-22T14:48:59.384Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/9296d9e97bfdef5483dfcc859d57b095b257144b2bc5300ab521e06f4bc7/wrapt-2.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e395f7bc31851ef9b612050368cb446e9bc14cd7454b025018980349caf25ae5", size = 156604, upload-time = "2026-05-22T14:49:00.921Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/16953929ed6776175720e58fc966e779926d8d71e2c7b2273230590ca71f/wrapt-2.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f1845c2a8cc1180ccccfa45785dd06f562730d19ef75be180334254012b6283", size = 166007, upload-time = "2026-05-22T14:49:02.332Z" }, + { url = "https://files.pythonhosted.org/packages/b9/73/20ee58c0612dae7c31131a7095345812ed2c7b389019e175f68cde34e5b4/wrapt-2.2.1-cp314-cp314-win32.whl", hash = "sha256:436addbc4bb4fc0a88c702577f51195d7d73683a7f3e0e5b253d8404d7847243", size = 78327, upload-time = "2026-05-22T14:49:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/22/b3/ef7c3295d02e0448a71c639a36a057f46d524d057c9486291a7a3039e65c/wrapt-2.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:50972a1d974ea07725a7f6b1cec5f8759008afd030a0024843ebe7d52de47f2b", size = 81144, upload-time = "2026-05-22T14:49:05.093Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dc/7bdf336953f99f4ceb0a584bb8870e42c8f26f93ea10c87834dad62f1668/wrapt-2.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1c9934ea5d92957e3cd0adbc0845539dccfd62710ebe16195a8c66c53954db36", size = 79569, upload-time = "2026-05-22T14:49:06.413Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6d/6dfae80150ff1919c356d1dd528f049bcdfaae29b4d284bc957e022caef4/wrapt-2.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17de18fc12cea55b8a9587314cb830573e37fb33b247a7515696350863714188", size = 82892, upload-time = "2026-05-22T14:49:07.925Z" }, + { url = "https://files.pythonhosted.org/packages/82/7b/4e34766a7d7804ffce9e71befe47e9b3225dc350c49c94493c4ab39fd3a5/wrapt-2.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9dec1aca52dddde7df94818310fa2fe79739c8f385b2014c4cb1035f5508199", size = 83333, upload-time = "2026-05-22T14:49:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/9d/57/0b34db3e8de44ccfece62d7b337abd1631dd810f5adc5f3db571727836b5/wrapt-2.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:69f2e9244542cb34dd59c7f073445b9e54ad9f3fce8d93606c368a1b499fc413", size = 202899, upload-time = "2026-05-22T14:49:10.572Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/ac0c459f154b99d92789a6cba7ca727185b83513b986f8ec7fe2aacddcbf/wrapt-2.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d83966dc7f4f45e8b97b5933685ac2e6e67fc0e19246ea314bceb9a8970c956", size = 209986, upload-time = "2026-05-22T14:49:12.229Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/77e37ff33ad018fa81ade52c25fa327b80b56f81d734279a63614fcb4cbc/wrapt-2.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78b0aa6bfb7be8deed0ab23e7aa028cc5210c29bc2d32a04d52b50e517a7307e", size = 194893, upload-time = "2026-05-22T14:49:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9d/7ea651d1ab032fc5fa222fbec91d0f8a1397f6ae04ebb93fa7219aa921d7/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:05d5cb74d1b232ec8cfa130a8f900708699ff2491d97b8f85a4cdc5996294b85", size = 205636, upload-time = "2026-05-22T14:49:15.714Z" }, + { url = "https://files.pythonhosted.org/packages/09/af/8e88031a701275b9085c54e64bc88c0b1cd55c77eadd400691c371cd76c4/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f6518b94edb9150452e9aba08027d4cc293433753ec1fbefb4629a21cbc74181", size = 192267, upload-time = "2026-05-22T14:49:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a8/e657ca876b06710194f243d81c4b0896ade646e244bdbec2d87c8c56a8bd/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed55af48b3eb28f43228ca2306788892bcb629eb2b5c4876e2a3659872c2f17a", size = 198378, upload-time = "2026-05-22T14:49:18.785Z" }, + { url = "https://files.pythonhosted.org/packages/c8/59/822efe4ea722a3961331bfa35b7d90937790d2c20f0616de1997ccc3aebd/wrapt-2.2.1-cp314-cp314t-win32.whl", hash = "sha256:2e08688ab16525897da6589d56d0aebaf417bbe91c2d8e3b96203b1efa596e85", size = 80226, upload-time = "2026-05-22T14:49:20.264Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/2a7dc5f6abb2fca0b6e1610e120419f603650aceb4f1d3ac4cae0354e162/wrapt-2.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fd0135d34387f5fd087d9be368ea77ea89cf2451dc1cd1c622d35021bcb3ab50", size = 83835, upload-time = "2026-05-22T14:49:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c0/782b86e28d1ceebeb74cccea12d2cd3d2ba0bd68e3dec20b1bc5873f6127/wrapt-2.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:f70db64e8266d7c45d3b735f2e08eeb434b5e03da9a479ae42b2e2e486a21a00", size = 80722, upload-time = "2026-05-22T14:49:23.59Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" }, ] [[package]] name = "yt-dlp" -version = "2026.3.17" +version = "2026.6.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/34/7c6b4e3f89cb6416d2cd7ab6dab141a1df97ab0fb22d15816db2c92148c9/yt_dlp-2026.3.17.tar.gz", hash = "sha256:ba7aa31d533f1ffccfe70e421596d7ca8ff0bf1398dc6bb658b7d9dec057d2c9", size = 3119221, upload-time = "2026-03-17T23:43:00.244Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/a4/1b0979d28f87774bb67fbbc66bce44f9dd1aa0e547a99e22985fac945c33/yt_dlp-2026.6.9.tar.gz", hash = "sha256:d50fcb95f48d61bedde33e408c1881d4c279e51c31354a599ce09e96ba0f4b86", size = 3030590, upload-time = "2026-06-09T23:27:14.831Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/13/5093bcb954878e50f7217fd2ab94282b53934022e4e4a03265582da83bf5/yt_dlp-2026.3.17-py3-none-any.whl", hash = "sha256:32992db94303a8a5d211a183f2174834fe7f8c29d83ed2e7a324eae97a8f26d8", size = 3315134, upload-time = "2026-03-17T23:42:57.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ee/188a3dadf9dfdac713243521f919feca1cd091d4358c9ea7e8ebb710a7cc/yt_dlp-2026.6.9-py3-none-any.whl", hash = "sha256:442ba4c75724b9496144c8434b617962ee08d0ee7c26ec663848fe9b78d5a3e4", size = 3169035, upload-time = "2026-06-09T23:27:12.58Z" }, ] [[package]] name = "zipp" -version = "3.23.1" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, ] From 299e1bc99d93a0ec63747b6923492b9e155c9e17 Mon Sep 17 00:00:00 2001 From: naraypv Date: Mon, 29 Jun 2026 17:18:20 -0400 Subject: [PATCH 2/5] fix: stabilize production merge --- graphify/build.py | 25 ++++++++++++++++--- graphify/llm.py | 2 +- graphify/skill-agents.md | 2 ++ graphify/skill-amp.md | 2 ++ graphify/skill-claw.md | 2 ++ graphify/skill-codex.md | 2 ++ graphify/skill-copilot.md | 2 ++ graphify/skill-droid.md | 2 ++ graphify/skill-kilo.md | 2 ++ graphify/skill-kiro.md | 2 ++ graphify/skill-opencode.md | 2 ++ graphify/skill-pi.md | 2 ++ graphify/skill-trae.md | 2 ++ graphify/skill-vscode.md | 2 ++ graphify/skill-windows.md | 2 ++ graphify/skill.md | 2 ++ tests/test_llm_backends.py | 7 +++++- .../expected/graphify__skill-agents.md | 2 ++ .../skillgen/expected/graphify__skill-amp.md | 2 ++ .../skillgen/expected/graphify__skill-claw.md | 2 ++ .../expected/graphify__skill-codex.md | 2 ++ .../expected/graphify__skill-copilot.md | 2 ++ .../expected/graphify__skill-droid.md | 2 ++ .../skillgen/expected/graphify__skill-kilo.md | 2 ++ .../skillgen/expected/graphify__skill-kiro.md | 2 ++ .../expected/graphify__skill-opencode.md | 2 ++ tools/skillgen/expected/graphify__skill-pi.md | 2 ++ .../skillgen/expected/graphify__skill-trae.md | 2 ++ .../expected/graphify__skill-vscode.md | 2 ++ .../expected/graphify__skill-windows.md | 2 ++ tools/skillgen/expected/graphify__skill.md | 2 ++ tools/skillgen/fragments/core/core.md | 2 ++ tools/skillgen/gen.py | 5 ++++ 33 files changed, 92 insertions(+), 5 deletions(-) diff --git a/graphify/build.py b/graphify/build.py index 46f1e4a7f..fcfa74079 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -143,6 +143,13 @@ def _canonicalize_extraction_schema(extraction: dict) -> None: dropped_edges = 0 coerced_ids = 0 + def _coerce_scalar_id(value) -> str | None: + if isinstance(value, str): + return value + if isinstance(value, (int, float, bool)): + return str(value) + return None + for node in extraction.get("nodes", []): if not isinstance(node, dict): dropped_nodes += 1 @@ -152,7 +159,11 @@ def _canonicalize_extraction_schema(extraction: dict) -> None: dropped_nodes += 1 continue if not isinstance(node_id, str): - node["id"] = str(node_id) + coerced = _coerce_scalar_id(node_id) + if coerced is None: + dropped_nodes += 1 + continue + node["id"] = coerced coerced_ids += 1 label = node.get("label") if not isinstance(label, str) or not label.strip(): @@ -178,10 +189,18 @@ def _canonicalize_extraction_schema(extraction: dict) -> None: dropped_edges += 1 continue if not isinstance(edge["source"], str): - edge["source"] = str(edge["source"]) + source_id = _coerce_scalar_id(edge["source"]) + if source_id is None: + dropped_edges += 1 + continue + edge["source"] = source_id coerced_ids += 1 if not isinstance(edge["target"], str): - edge["target"] = str(edge["target"]) + target_id = _coerce_scalar_id(edge["target"]) + if target_id is None: + dropped_edges += 1 + continue + edge["target"] = target_id coerced_ids += 1 if not isinstance(edge["relation"], str): edge["relation"] = str(edge["relation"]) diff --git a/graphify/llm.py b/graphify/llm.py index 8a59fdb50..ff918c63d 100644 --- a/graphify/llm.py +++ b/graphify/llm.py @@ -141,7 +141,7 @@ def _get_tokenizer(): "env_key": "OLLAMA_API_KEY", "pricing": {"input": 0.0, "output": 0.0}, "temperature": 0, - "max_tokens": 8192, + "max_tokens": 16384, }, "gemini": { # GEMINI_BASE_URL points the backend at any OpenAI-compatible server for diff --git a/graphify/skill-agents.md b/graphify/skill-agents.md index cc869e5d7..7dcc7722e 100644 --- a/graphify/skill-agents.md +++ b/graphify/skill-agents.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill-amp.md b/graphify/skill-amp.md index cc869e5d7..7dcc7722e 100644 --- a/graphify/skill-amp.md +++ b/graphify/skill-amp.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill-claw.md b/graphify/skill-claw.md index 594888f96..4ec4ab1f1 100644 --- a/graphify/skill-claw.md +++ b/graphify/skill-claw.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill-codex.md b/graphify/skill-codex.md index 093181217..9cf67ce0d 100644 --- a/graphify/skill-codex.md +++ b/graphify/skill-codex.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill-copilot.md b/graphify/skill-copilot.md index 594888f96..4ec4ab1f1 100644 --- a/graphify/skill-copilot.md +++ b/graphify/skill-copilot.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill-droid.md b/graphify/skill-droid.md index 83f7c80cc..df396ddd9 100644 --- a/graphify/skill-droid.md +++ b/graphify/skill-droid.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill-kilo.md b/graphify/skill-kilo.md index 6d2509e6b..d903c32a0 100644 --- a/graphify/skill-kilo.md +++ b/graphify/skill-kilo.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill-kiro.md b/graphify/skill-kiro.md index 594888f96..4ec4ab1f1 100644 --- a/graphify/skill-kiro.md +++ b/graphify/skill-kiro.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill-opencode.md b/graphify/skill-opencode.md index e1d1c97ac..a1ba829ec 100644 --- a/graphify/skill-opencode.md +++ b/graphify/skill-opencode.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill-pi.md b/graphify/skill-pi.md index 594888f96..4ec4ab1f1 100644 --- a/graphify/skill-pi.md +++ b/graphify/skill-pi.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill-trae.md b/graphify/skill-trae.md index f2677301b..45ef33d76 100644 --- a/graphify/skill-trae.md +++ b/graphify/skill-trae.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill-vscode.md b/graphify/skill-vscode.md index f9d28b97d..5a54a7b8a 100644 --- a/graphify/skill-vscode.md +++ b/graphify/skill-vscode.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill-windows.md b/graphify/skill-windows.md index c9f9367a2..664dfd4a1 100644 --- a/graphify/skill-windows.md +++ b/graphify/skill-windows.md @@ -173,6 +173,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/graphify/skill.md b/graphify/skill.md index 594888f96..4ec4ab1f1 100644 --- a/graphify/skill.md +++ b/graphify/skill.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tests/test_llm_backends.py b/tests/test_llm_backends.py index 3b30c5748..6d5cc72c7 100644 --- a/tests/test_llm_backends.py +++ b/tests/test_llm_backends.py @@ -298,7 +298,12 @@ def test_openai_compat_backends_resolve_full_output_cap(tmp_path, monkeypatch, b source.write_text("# Architecture\n") result = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 1, "output_tokens": 1} - with patch("graphify.llm._call_openai_compat", return_value=result) as call: + call_target = ( + "graphify.llm._call_ollama_native" + if backend == "ollama" + else "graphify.llm._call_openai_compat" + ) + with patch(call_target, return_value=result) as call: llm.extract_files_direct([source], backend=backend, root=tmp_path) assert call.call_args.kwargs["max_completion_tokens"] == 16384 diff --git a/tools/skillgen/expected/graphify__skill-agents.md b/tools/skillgen/expected/graphify__skill-agents.md index cc869e5d7..7dcc7722e 100644 --- a/tools/skillgen/expected/graphify__skill-agents.md +++ b/tools/skillgen/expected/graphify__skill-agents.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill-amp.md b/tools/skillgen/expected/graphify__skill-amp.md index cc869e5d7..7dcc7722e 100644 --- a/tools/skillgen/expected/graphify__skill-amp.md +++ b/tools/skillgen/expected/graphify__skill-amp.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill-claw.md b/tools/skillgen/expected/graphify__skill-claw.md index 594888f96..4ec4ab1f1 100644 --- a/tools/skillgen/expected/graphify__skill-claw.md +++ b/tools/skillgen/expected/graphify__skill-claw.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill-codex.md b/tools/skillgen/expected/graphify__skill-codex.md index 093181217..9cf67ce0d 100644 --- a/tools/skillgen/expected/graphify__skill-codex.md +++ b/tools/skillgen/expected/graphify__skill-codex.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill-copilot.md b/tools/skillgen/expected/graphify__skill-copilot.md index 594888f96..4ec4ab1f1 100644 --- a/tools/skillgen/expected/graphify__skill-copilot.md +++ b/tools/skillgen/expected/graphify__skill-copilot.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill-droid.md b/tools/skillgen/expected/graphify__skill-droid.md index 83f7c80cc..df396ddd9 100644 --- a/tools/skillgen/expected/graphify__skill-droid.md +++ b/tools/skillgen/expected/graphify__skill-droid.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill-kilo.md b/tools/skillgen/expected/graphify__skill-kilo.md index 6d2509e6b..d903c32a0 100644 --- a/tools/skillgen/expected/graphify__skill-kilo.md +++ b/tools/skillgen/expected/graphify__skill-kilo.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill-kiro.md b/tools/skillgen/expected/graphify__skill-kiro.md index 594888f96..4ec4ab1f1 100644 --- a/tools/skillgen/expected/graphify__skill-kiro.md +++ b/tools/skillgen/expected/graphify__skill-kiro.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill-opencode.md b/tools/skillgen/expected/graphify__skill-opencode.md index e1d1c97ac..a1ba829ec 100644 --- a/tools/skillgen/expected/graphify__skill-opencode.md +++ b/tools/skillgen/expected/graphify__skill-opencode.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill-pi.md b/tools/skillgen/expected/graphify__skill-pi.md index 594888f96..4ec4ab1f1 100644 --- a/tools/skillgen/expected/graphify__skill-pi.md +++ b/tools/skillgen/expected/graphify__skill-pi.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill-trae.md b/tools/skillgen/expected/graphify__skill-trae.md index f2677301b..45ef33d76 100644 --- a/tools/skillgen/expected/graphify__skill-trae.md +++ b/tools/skillgen/expected/graphify__skill-trae.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill-vscode.md b/tools/skillgen/expected/graphify__skill-vscode.md index f9d28b97d..5a54a7b8a 100644 --- a/tools/skillgen/expected/graphify__skill-vscode.md +++ b/tools/skillgen/expected/graphify__skill-vscode.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill-windows.md b/tools/skillgen/expected/graphify__skill-windows.md index c9f9367a2..664dfd4a1 100644 --- a/tools/skillgen/expected/graphify__skill-windows.md +++ b/tools/skillgen/expected/graphify__skill-windows.md @@ -173,6 +173,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/expected/graphify__skill.md b/tools/skillgen/expected/graphify__skill.md index 594888f96..4ec4ab1f1 100644 --- a/tools/skillgen/expected/graphify__skill.md +++ b/tools/skillgen/expected/graphify__skill.md @@ -151,6 +151,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/fragments/core/core.md b/tools/skillgen/fragments/core/core.md index e90c2b7f4..84daa5cc6 100644 --- a/tools/skillgen/fragments/core/core.md +++ b/tools/skillgen/fragments/core/core.md @@ -110,6 +110,8 @@ Skip this step entirely if `detect` returned zero `video` files. When the corpus This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). +> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so go straight to Part A and skip Part B. Semantic extraction starts on local Ollama when available and can fall back to MiniMax only when configured. If you cannot dispatch subagents and neither local Ollama nor MiniMax can run, do not stall: a code-only corpus has no semantic work (write the empty semantic file and continue to Part C); for docs/papers/images, extract them inline yourself. If you catch yourself about to prompt for or block on a missing API key, that is a misread of this skill — proceed without one. + **Before dispatching subagents:** prefer graphify's direct local backend path. Use `graphify.llm.extract_corpus_parallel(files, backend="ollama", allow_minimax_fallback=True, max_concurrency=1)` for semantic extraction when local Ollama is available. The default fallback order is local `qwen2.5-coder:3b`, then local `gemma3:4b`, then MiniMax when configured; keep local Ollama models within the laptop-safe <=8B class and route larger or contended semantic work to MiniMax. MiniMax is the token-plan fallback, not the default workhorse. Graphify starts semantic chunks on local Ollama and spills only a capped fraction to MiniMax when local chunks are slow, fail through the local model chain, or laptop CPU/GPU load is high. Tune with `GRAPHIFY_OLLAMA_BALANCE=auto|local|remote|defer`, `GRAPHIFY_OLLAMA_FALLBACK_MODELS=qwen2.5-coder:3b,gemma3:4b`, and `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` (default 0.25). Set `GRAPHIFY_DISABLE_MINIMAX_FALLBACK=1` for strict local-only runs. NVIDIA NIM is explicit-only (`--backend nim`) and is not part of the automatic graphify path. diff --git a/tools/skillgen/gen.py b/tools/skillgen/gen.py index 7b198d188..54ea0a657 100644 --- a/tools/skillgen/gen.py +++ b/tools/skillgen/gen.py @@ -864,6 +864,10 @@ def _is_no_api_key_fix_line(line: str) -> bool: return "graphify needs no API key" in line +def _is_credential_guard_safe_placeholder_line(line: str) -> bool: + return "push_to_neo4j(" in line and "NEO4J_PASSWORD" in line + + # Every line that may differ between a rendered monolith and its pristine v8 # baseline. Each predicate documents one sanctioned change-class; a blank line is # allowed because the multi-line fix blocks insert spacing. Anything else failing @@ -878,6 +882,7 @@ def _is_no_api_key_fix_line(line: str) -> bool: _is_zero_node_guard_fix_line, _is_manifest_root_fix_line, _is_no_api_key_fix_line, + _is_credential_guard_safe_placeholder_line, ) From b5397a8eb717100b99c72a4d8fb6919b0847ccc2 Mon Sep 17 00:00:00 2001 From: naraypv Date: Mon, 29 Jun 2026 18:34:37 -0400 Subject: [PATCH 3/5] fix: harden ollama fallback and worker limits --- graphify/__main__.py | 2 +- graphify/extract.py | 51 +++++++++++++++----------------- graphify/llm.py | 69 +++++++++++++++++++++++++++++++++++++++++++ tests/test_extract.py | 16 ++++++++++ tests/test_ollama.py | 67 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 177 insertions(+), 28 deletions(-) diff --git a/graphify/__main__.py b/graphify/__main__.py index b145d0093..4b1bfa9bc 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -2313,7 +2313,7 @@ def main() -> None: print(" proxy, gateways): set ANTHROPIC_BASE_URL and ANTHROPIC_MODEL") print(" --model M override backend default model") print(" --mode deep aggressive INFERRED-edge semantic extraction") - print(" --max-workers N AST extraction subprocess count (default: cpu_count)") + print(" --max-workers N AST extraction subprocess count (default: half CPUs, capped at 8)") print(" --token-budget N per-chunk token cap for semantic extraction (default: 60000)") print(" --max-concurrency N parallel semantic chunks in flight (default: 4; set 1 for local LLMs)") print(" --api-timeout S per-request timeout in seconds for the LLM client (default: 600)") diff --git a/graphify/extract.py b/graphify/extract.py index c47f2c0cc..a6f2bcd19 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -13303,31 +13303,7 @@ def _extract_parallel( """ import concurrent.futures - if max_workers is None: - # Honour GRAPHIFY_MAX_WORKERS env override; otherwise scale to the - # full CPU. The historical `, 8)` cap was a safety bound for laptops - # in 2023 — on a 32-thread workstation it costs a 4x slowdown - # (issue #792). Capping at len(uncached_work) keeps small jobs - # from spawning useless idle workers. - env_raw = os.environ.get("GRAPHIFY_MAX_WORKERS", "").strip() - env_cap = None - if env_raw: - try: - v = int(env_raw) - if v > 0: - env_cap = v - except ValueError: - pass - cpu_cap = env_cap if env_cap is not None else (os.cpu_count() or 4) - max_workers = min(cpu_cap, len(uncached_work)) - - # Windows ProcessPoolExecutor hard-caps at 61 workers (CPython limitation - # tied to WaitForMultipleObjects). Clamp here so every path — auto-compute, - # GRAPHIFY_MAX_WORKERS, and --max-workers — stays valid on >61-core boxes - # (issue #1298). Guard against 0 from an empty work list. - if sys.platform == "win32": - max_workers = min(max_workers, 61) - max_workers = max(max_workers, 1) + max_workers = _resolve_max_workers(max_workers, len(uncached_work)) root_str = str(effective_root) work_items = [(idx, str(path), root_str) for idx, path in uncached_work] @@ -13416,6 +13392,27 @@ def _extract_sequential( _PARALLEL_THRESHOLD = 20 +def _resolve_max_workers(max_workers: int | None, uncached_count: int) -> int: + if max_workers is None: + env_raw = os.environ.get("GRAPHIFY_MAX_WORKERS", "").strip() + env_cap = None + if env_raw: + try: + v = int(env_raw) + if v > 0: + env_cap = v + except ValueError: + pass + if env_cap is None: + cpu_count = os.cpu_count() or 4 + env_cap = max(1, min(8, cpu_count // 2)) + max_workers = min(env_cap, uncached_count) + + if sys.platform == "win32": + max_workers = min(max_workers, 61) + return max(max_workers, 1) + + def extract( paths: list[Path], cache_root: Path | None = None, @@ -13437,8 +13434,8 @@ def extract( subdirectory so the cache stays at ./graphify-out/cache/. parallel: if True and there are >= _PARALLEL_THRESHOLD uncached files, use ProcessPoolExecutor for multi-core extraction. - max_workers: max subprocess count. Defaults to cpu_count (or the - value of GRAPHIFY_MAX_WORKERS if set), bounded by len(uncached_work). + max_workers: max subprocess count. Defaults to half the CPU count + capped at 8 (or GRAPHIFY_MAX_WORKERS if set), bounded by len(uncached_work). """ paths = [Path(p) for p in paths] _check_tree_sitter_version() diff --git a/graphify/llm.py b/graphify/llm.py index ff918c63d..fa764e9a9 100644 --- a/graphify/llm.py +++ b/graphify/llm.py @@ -6,6 +6,7 @@ import base64 import hashlib +import importlib.util import json import os import re @@ -54,6 +55,9 @@ _OLLAMA_GPU_UTIL_THRESHOLD = 85 _OLLAMA_GPU_MEM_THRESHOLD = 0.90 +_BACKEND_UNAVAILABLE_WARNED: set[str] = set() +_OPENAI_COMPAT_BACKENDS = {"minimax", "nim", "kimi", "gemini", "openai", "deepseek"} + @@ -1071,6 +1075,33 @@ def _backend_pkg_hint(pkg: str, extra: str) -> str: f"(uv tool), or pip install {pkg} (pip/venv install)." ) + +def _module_available(name: str) -> bool: + if name in sys.modules and sys.modules[name] is not None: + return True + return importlib.util.find_spec(name) is not None + + +def _backend_runtime_unavailable_reason(backend: str) -> str | None: + if backend in _OPENAI_COMPAT_BACKENDS and not _module_available("openai"): + return _backend_pkg_hint("openai", backend) + if backend == "claude" and not _module_available("anthropic"): + return _backend_pkg_hint("anthropic", "anthropic") + if backend == "bedrock" and not _module_available("boto3"): + return "AWS Bedrock extraction requires boto3. Run: pip install graphifyy[bedrock]" + return None + + +def _warn_backend_unavailable_once(backend: str, reason: str) -> None: + if backend in _BACKEND_UNAVAILABLE_WARNED: + return + _BACKEND_UNAVAILABLE_WARNED.add(backend) + print( + f"[graphify] {backend} fallback disabled for this run: {reason}", + file=sys.stderr, + ) + + def _automatic_fallback_backend(backend: str, *, allow: bool, model: str | None = None) -> str | None: """Return the configured automatic fallback for an auto-selected backend.""" if not allow: @@ -1079,6 +1110,10 @@ def _automatic_fallback_backend(backend: str, *, allow: bool, model: str | None if os.environ.get("GRAPHIFY_DISABLE_MINIMAX_FALLBACK", "").strip().lower() in ("1", "true", "yes"): return None if _get_backend_api_key("minimax"): + reason = _backend_runtime_unavailable_reason("minimax") + if reason: + _warn_backend_unavailable_once("minimax", reason) + return None return "minimax" return None @@ -2421,6 +2456,19 @@ def _route_for_chunk(idx: int) -> tuple[str, str | None, str | None]: return str(fallback), None, None return "ollama", api_key, model + def _disable_spill_backend(run_backend: str, exc: Exception) -> None: + if ollama_balance is None: + return + if run_backend == "ollama": + return + ollama_balance["fallback"] = None + ollama_balance["remote_cap"] = 0 + print( + f"[graphify] {run_backend} spill failed ({type(exc).__name__}: {exc}); " + "disabling remote spill for this run and retrying the chunk locally.", + file=sys.stderr, + ) + def _run_one(idx: int, chunk: list[Path]) -> tuple[int, dict | None, Exception | None]: run_backend, run_api_key, run_model = _route_for_chunk(idx) t0 = time.time() @@ -2443,6 +2491,27 @@ def _run_one(idx: int, chunk: list[Path]) -> tuple[int, dict | None, Exception | ollama_balance["last_local_seconds"] = elapsed return idx, result, None except Exception as exc: # noqa: BLE001 — caller-facing surface, log + continue + if backend == "ollama" and run_backend != "ollama" and ollama_balance is not None: + _disable_spill_backend(run_backend, exc) + retry_t0 = time.time() + try: + result = _extract_with_adaptive_retry( + chunk, + backend="ollama", + model=model, + root=root, + max_depth=max_retry_depth, + deep_mode=deep_mode, + allow_minimax_fallback=False, + **{"api_key": api_key}, + ) + elapsed = round(time.time() - retry_t0, 2) + result["elapsed_seconds"] = elapsed + result["backend"] = result.get("backend") or "ollama" + ollama_balance["last_local_seconds"] = elapsed + return idx, result, None + except Exception as local_exc: # noqa: BLE001 — preserve loud chunk accounting + return idx, None, local_exc return idx, None, exc # Ollama serves one request at a time per loaded model on a single GPU. diff --git a/tests/test_extract.py b/tests/test_extract.py index 2f01bc0fd..b4d818c69 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -982,6 +982,22 @@ def wrapped_sequential(*args, **kwargs): assert result["nodes"], "extract should still produce nodes after fallback" +def test_default_ast_workers_are_bounded_for_background_use(monkeypatch): + from graphify import extract as extract_mod + + monkeypatch.delenv("GRAPHIFY_MAX_WORKERS", raising=False) + monkeypatch.setattr(os, "cpu_count", lambda: 32) + assert extract_mod._resolve_max_workers(None, 10_000) == 8 + + +def test_graphify_max_workers_env_overrides_background_default(monkeypatch): + from graphify import extract as extract_mod + + monkeypatch.setenv("GRAPHIFY_MAX_WORKERS", "3") + monkeypatch.setattr(os, "cpu_count", lambda: 32) + assert extract_mod._resolve_max_workers(None, 10_000) == 3 + + def test_extract_parallel_returns_false_on_broken_pool(tmp_path, monkeypatch, capsys): """_extract_parallel must catch BrokenProcessPool internally and return False.""" from concurrent.futures.process import BrokenProcessPool diff --git a/tests/test_ollama.py b/tests/test_ollama.py index bf5e673a7..203e83359 100644 --- a/tests/test_ollama.py +++ b/tests/test_ollama.py @@ -54,6 +54,73 @@ def test_ollama_in_backends(): assert BACKENDS["ollama"]["pricing"]["output"] == 0.0 assert "max_tokens" in BACKENDS["ollama"] + +def test_minimax_fallback_disabled_when_openai_sdk_missing(monkeypatch, capsys): + from graphify import llm + + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") + monkeypatch.setattr(llm, "_module_available", lambda name: name != "openai") + llm._BACKEND_UNAVAILABLE_WARNED.clear() + + assert llm._automatic_fallback_backend("ollama", allow=True) is None + err = capsys.readouterr().err + assert "minimax fallback disabled" in err.lower() + assert "openai" in err.lower() + + +def test_failed_minimax_spill_retries_locally_and_disables_spill(monkeypatch, tmp_path, capsys): + from graphify import llm + + for key in ( + "GRAPHIFY_OLLAMA_BALANCE", + "GRAPHIFY_OLLAMA_DAYTIME_FILE_LIMIT", + "GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION", + ): + monkeypatch.delenv(key, raising=False) + monkeypatch.setenv("GRAPHIFY_OLLAMA_BALANCE", "remote") + monkeypatch.setenv("GRAPHIFY_OLLAMA_DAYTIME_FILE_LIMIT", "1") + monkeypatch.setenv("GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION", "1") + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") + monkeypatch.setattr(llm, "_backend_runtime_unavailable_reason", lambda backend: None) + + files = [] + for idx in range(2): + path = tmp_path / f"f{idx}.md" + path.write_text(f"file {idx}", encoding="utf-8") + files.append(path) + + calls = [] + + def fake_extract(chunk, **kwargs): + backend = kwargs["backend"] + calls.append(backend) + if backend == "minimax": + raise ImportError("missing openai") + return { + "nodes": [{"id": f"n{len(calls)}", "label": "N", "file_type": "document", "source_file": str(chunk[0])}], + "edges": [], + "hyperedges": [], + "input_tokens": 1, + "output_tokens": 1, + } + + monkeypatch.setattr(llm, "_extract_with_adaptive_retry", fake_extract) + + result = llm.extract_corpus_parallel( + files, + backend="ollama", + token_budget=None, + chunk_size=1, + max_concurrency=1, + allow_minimax_fallback=True, + ) + + assert calls == ["minimax", "ollama", "ollama"] + assert result["failed_chunks"] == 0 + assert result["minimax_chunks"] == 0 + assert len(result["nodes"]) == 2 + assert "disabling remote spill" in capsys.readouterr().err + def _clear_non_ollama_keys(monkeypatch): for key in ( "MINIMAX_API_KEY", "GRAPHIFY_MINIMAX_API_KEY", From cc69a78c8c31724123e2b9dd8f614853bd7568ee Mon Sep 17 00:00:00 2001 From: naraypv Date: Mon, 13 Jul 2026 18:33:06 -0400 Subject: [PATCH 4/5] chore: sync production with upstream v0.9.14 --- BENCHMARKS.md | 187 + CHANGELOG.md | 206 + README.md | 235 +- SECURITY.md | 2 +- docs/demo-path.svg | 1 + docs/graph-hero.png | Bin 0 -> 648908 bytes docs/logo.png | Bin 0 -> 146843 bytes docs/translations/README.ar-SA.md | 2 +- docs/translations/README.de-DE.md | 2 +- docs/translations/README.es-ES.md | 2 +- docs/translations/README.fa-IR.md | 8 +- docs/translations/README.fil-PH.md | 2 +- docs/translations/README.fr-FR.md | 2 +- docs/translations/README.he-IL.md | 849 ++ docs/translations/README.hi-IN.md | 2 +- docs/translations/README.pt-BR.md | 2 +- docs/translations/README.ru-RU.md | 2 +- docs/translations/README.uk-UA.md | 8 +- docs/translations/README.uz-UZ.md | 2 +- graphify/__main__.py | 4916 +----- graphify/affected.py | 30 +- graphify/analyze.py | 14 +- graphify/build.py | 409 +- graphify/cache.py | 160 +- graphify/cli.py | 2789 ++++ graphify/cluster.py | 48 + graphify/dedup.py | 22 +- graphify/detect.py | 264 +- graphify/diagnostics.py | 8 +- graphify/export.py | 758 +- graphify/exporters/__init__.py | 1 + graphify/exporters/base.py | 14 + graphify/exporters/graphdb.py | 173 + graphify/exporters/html.py | 547 + graphify/extract.py | 12485 +++------------- graphify/extractors/MIGRATION.md | 16 +- graphify/extractors/__init__.py | 41 + graphify/extractors/apex.py | 215 + graphify/extractors/base.py | 9 +- graphify/extractors/bash.py | 248 + graphify/extractors/csharp.py | 459 +- graphify/extractors/dart.py | 528 + graphify/extractors/dm.py | 494 + graphify/extractors/elixir.py | 32 +- graphify/extractors/engine.py | 4519 ++++++ graphify/extractors/fortran.py | 309 + graphify/extractors/go.py | 396 + graphify/extractors/json_config.py | 209 + graphify/extractors/julia.py | 275 + graphify/extractors/markdown.py | 176 + graphify/extractors/models.py | 119 + graphify/extractors/objc.py | 430 + graphify/extractors/pascal.py | 688 + graphify/extractors/pascal_forms.py | 196 + graphify/extractors/powershell.py | 496 + graphify/extractors/resolution.py | 2292 +++ graphify/extractors/rust.py | 410 + graphify/extractors/sln.py | 92 + graphify/extractors/sql.py | 276 + graphify/extractors/terraform.py | 181 + graphify/extractors/verilog.py | 329 + graphify/hooks.py | 90 +- graphify/install.py | 2148 +++ graphify/llm.py | 504 +- graphify/pascal_resolution.py | 129 + graphify/paths.py | 190 +- graphify/pg_introspect.py | 63 +- graphify/querylog.py | 12 +- graphify/reflect.py | 309 +- graphify/report.py | 118 +- graphify/ruby_resolution.py | 58 +- graphify/security.py | 3 +- graphify/semantic_cleanup.py | 10 + graphify/serve.py | 349 +- graphify/skill-agents.md | 13 +- graphify/skill-aider.md | 11 +- graphify/skill-amp.md | 13 +- graphify/skill-claw.md | 13 +- graphify/skill-codex.md | 13 +- graphify/skill-copilot.md | 13 +- graphify/skill-devin.md | 11 +- graphify/skill-droid.md | 13 +- graphify/skill-kilo.md | 13 +- graphify/skill-kiro.md | 13 +- graphify/skill-opencode.md | 13 +- graphify/skill-pi.md | 13 +- graphify/skill-trae.md | 13 +- graphify/skill-vscode.md | 13 +- graphify/skill-windows.md | 9 +- graphify/skill.md | 13 +- graphify/skills/agents/references/query.md | 10 +- graphify/skills/amp/references/query.md | 10 +- graphify/skills/claude/references/query.md | 10 +- graphify/skills/claw/references/query.md | 10 +- graphify/skills/codex/references/query.md | 10 +- graphify/skills/copilot/references/query.md | 10 +- graphify/skills/droid/references/query.md | 10 +- graphify/skills/kilo/references/query.md | 10 +- graphify/skills/kiro/references/query.md | 10 +- graphify/skills/opencode/references/query.md | 10 +- graphify/skills/pi/references/query.md | 10 +- graphify/skills/trae/references/query.md | 10 +- graphify/skills/vscode/references/query.md | 10 +- graphify/skills/windows/references/query.md | 10 +- graphify/symbol_resolution.py | 24 +- graphify/watch.py | 497 +- pyproject.toml | 11 +- scripts/gen_demo_path.py | 236 + tests/fixtures/cpp_logger/a/Logger.cpp | 3 + tests/fixtures/cpp_logger/a/Logger.h | 4 + tests/fixtures/cpp_logger/b/Logger.cpp | 3 + tests/fixtures/cpp_logger/b/Logger.h | 4 + tests/fixtures/cpp_paired/Foo.cpp | 5 + tests/fixtures/cpp_paired/Foo.h | 10 + tests/fixtures/cpp_paired/Main.cpp | 7 + tests/fixtures/cpp_samedir/Alpha.h | 4 + tests/fixtures/cpp_samedir/Beta.h | 4 + tests/fixtures/cpp_samedir/plain.h | 7 + tests/fixtures/objc_mixed/Bridging-Header.h | 1 + tests/fixtures/objc_mixed/Widget.h | 4 + tests/fixtures/objc_mixed/Widget.m | 9 + tests/fixtures/objc_mixed/WidgetExtras.swift | 5 + .../fixtures/pascal_cross_file/BaseGadget.pas | 18 + .../pascal_cross_file/DerivedGadget.pas | 21 + .../pascal_cross_file/OtherGadget.pas | 18 + tests/fixtures/sample.cpp | 11 + tests/fixtures/sample.cs | 4 + tests/fixtures/sample.ex | 1 + tests/fixtures/sample.f90 | 13 + tests/fixtures/sample.groovy | 14 + tests/fixtures/sample.java | 11 + tests/fixtures/sample.jl | 2 + tests/fixtures/sample.kt | 8 + tests/fixtures/sample.m | 12 + tests/fixtures/sample.php | 9 +- tests/fixtures/sample.ps1 | 16 + tests/fixtures/sample.rb | 6 + tests/fixtures/sample.rs | 7 + tests/fixtures/sample.scala | 1 + tests/fixtures/sample.sv | 5 + tests/fixtures/sample.swift | 1 + tests/fixtures/sample_scoped_calls.pas | 60 + tests/fixtures/typescript_advanced.ts | 4 + tests/test_affected_member_seed.py | 61 + tests/test_analyze.py | 15 +- tests/test_build.py | 221 + .../test_build_merge_hyperedges_and_prune.py | 209 + tests/test_builtin_global_type_refs.py | 83 + tests/test_cache.py | 76 + tests/test_case_sensitive_resolution.py | 87 + tests/test_chunking.py | 125 +- tests/test_claude_cli_backend.py | 37 + tests/test_claude_md.py | 69 + tests/test_cli_broken_pipe.py | 49 + tests/test_cli_export.py | 87 + tests/test_community_hub_labels.py | 83 + tests/test_corrupt_graph_json.py | 53 + tests/test_cpp_objc_cross_file_calls.py | 263 + tests/test_cross_language_call_resolution.py | 103 + tests/test_csharp_member_calls.py | 167 + tests/test_csharp_type_resolution.py | 397 + tests/test_dedup.py | 40 + tests/test_detect.py | 189 +- tests/test_dotnet.py | 29 + tests/test_explain_cli.py | 43 + tests/test_export.py | 162 + tests/test_extract.py | 533 +- tests/test_extract_cache_location.py | 141 + tests/test_extract_cli.py | 12 + tests/test_extract_code_only_cli.py | 77 + tests/test_extractors_registry.py | 44 + tests/test_gemini_hook.py | 71 + tests/test_hook_guard.py | 280 + tests/test_hooks.py | 117 +- tests/test_hypergraph.py | 93 + tests/test_image_vision.py | 31 + tests/test_indirect_dispatch.py | 506 + tests/test_indirect_dispatch_assign_return.py | 133 + tests/test_indirect_dispatch_getattr.py | 153 + tests/test_install.py | 47 + tests/test_install_strings.py | 12 +- tests/test_install_upgrade.py | 9 +- tests/test_java_member_calls.py | 278 + tests/test_java_type_resolution.py | 103 + tests/test_js_import_resolution.py | 297 + tests/test_labeling.py | 68 +- tests/test_languages.py | 643 +- tests/test_llm_backends.py | 47 + tests/test_llm_parser.py | 29 +- tests/test_long_path_hashing.py | 50 + tests/test_merge_graphs_cli.py | 94 + tests/test_multilang.py | 26 + tests/test_obsidian_dangling_member.py | 20 + tests/test_office_incremental.py | 67 + tests/test_ollama_retry_cap.py | 60 + tests/test_pascal.py | 23 + tests/test_pascal_call_scoping.py | 102 + tests/test_pascal_resolution.py | 95 + tests/test_path_cli.py | 57 + tests/test_paths.py | 99 + tests/test_pg_introspect.py | 55 +- tests/test_phantom_cross_package_call.py | 87 + tests/test_phantom_external_import.py | 116 + tests/test_querylog.py | 45 + tests/test_rationale.py | 69 + tests/test_read_hook.py | 46 +- tests/test_reflect.py | 246 + tests/test_replace_or_append_section.py | 62 + tests/test_report.py | 93 + tests/test_ruby_resolution.py | 173 + tests/test_search_hook.py | 111 + tests/test_security.py | 58 + tests/test_semantic_cleanup.py | 33 + tests/test_semantic_fragment_sanitize.py | 71 + tests/test_semantic_id_remap_root.py | 50 + tests/test_serve.py | 244 +- tests/test_serve_http.py | 69 + tests/test_skill_version_warning.py | 60 + tests/test_skillgen.py | 20 +- tests/test_swift_cross_file_calls.py | 25 + tests/test_symbol_resolution.py | 94 +- tests/test_ts_decorators.py | 153 + tests/test_ts_generators.py | 74 + tests/test_ts_import_require.py | 119 + tests/test_ts_namespace.py | 73 + tests/test_ts_receiver_member_calls.py | 81 + tests/test_typescript_module_extensions.py | 84 + tests/test_watch.py | 537 + tests/test_word_count_cache.py | 52 + tests/test_zero_node_no_cache.py | 54 + .../expected/graphify__skill-agents.md | 13 +- .../expected/graphify__skill-aider.md | 11 +- .../skillgen/expected/graphify__skill-amp.md | 13 +- .../skillgen/expected/graphify__skill-claw.md | 13 +- .../expected/graphify__skill-codex.md | 13 +- .../expected/graphify__skill-copilot.md | 13 +- .../expected/graphify__skill-devin.md | 11 +- .../expected/graphify__skill-droid.md | 13 +- .../skillgen/expected/graphify__skill-kilo.md | 13 +- .../skillgen/expected/graphify__skill-kiro.md | 13 +- .../expected/graphify__skill-opencode.md | 13 +- tools/skillgen/expected/graphify__skill-pi.md | 13 +- .../skillgen/expected/graphify__skill-trae.md | 13 +- .../expected/graphify__skill-vscode.md | 13 +- .../expected/graphify__skill-windows.md | 9 +- tools/skillgen/expected/graphify__skill.md | 13 +- ...hify__skills__agents__references__query.md | 10 +- ...raphify__skills__amp__references__query.md | 10 +- ...hify__skills__claude__references__query.md | 10 +- ...aphify__skills__claw__references__query.md | 10 +- ...phify__skills__codex__references__query.md | 10 +- ...ify__skills__copilot__references__query.md | 10 +- ...phify__skills__droid__references__query.md | 10 +- ...aphify__skills__kilo__references__query.md | 10 +- ...aphify__skills__kiro__references__query.md | 10 +- ...fy__skills__opencode__references__query.md | 10 +- ...graphify__skills__pi__references__query.md | 10 +- ...aphify__skills__trae__references__query.md | 10 +- ...hify__skills__vscode__references__query.md | 10 +- ...ify__skills__windows__references__query.md | 10 +- tools/skillgen/fragments/core/aider.md | 11 +- tools/skillgen/fragments/core/core.md | 7 +- tools/skillgen/fragments/core/devin.md | 11 +- .../fragments/references/query/default.md | 10 +- tools/skillgen/fragments/shell/posix.md | 6 +- tools/skillgen/gen.py | 63 +- tools/skillgen/platforms.toml | 8 +- uv.lock | 30 +- 268 files changed, 36367 insertions(+), 17228 deletions(-) create mode 100644 BENCHMARKS.md create mode 100644 docs/demo-path.svg create mode 100644 docs/graph-hero.png create mode 100644 docs/logo.png create mode 100644 docs/translations/README.he-IL.md create mode 100644 graphify/cli.py create mode 100644 graphify/exporters/__init__.py create mode 100644 graphify/exporters/base.py create mode 100644 graphify/exporters/graphdb.py create mode 100644 graphify/exporters/html.py create mode 100644 graphify/extractors/apex.py create mode 100644 graphify/extractors/bash.py create mode 100644 graphify/extractors/dart.py create mode 100644 graphify/extractors/dm.py create mode 100644 graphify/extractors/engine.py create mode 100644 graphify/extractors/fortran.py create mode 100644 graphify/extractors/go.py create mode 100644 graphify/extractors/json_config.py create mode 100644 graphify/extractors/julia.py create mode 100644 graphify/extractors/markdown.py create mode 100644 graphify/extractors/models.py create mode 100644 graphify/extractors/objc.py create mode 100644 graphify/extractors/pascal.py create mode 100644 graphify/extractors/pascal_forms.py create mode 100644 graphify/extractors/powershell.py create mode 100644 graphify/extractors/resolution.py create mode 100644 graphify/extractors/rust.py create mode 100644 graphify/extractors/sln.py create mode 100644 graphify/extractors/sql.py create mode 100644 graphify/extractors/terraform.py create mode 100644 graphify/extractors/verilog.py create mode 100644 graphify/install.py create mode 100644 graphify/pascal_resolution.py create mode 100644 scripts/gen_demo_path.py create mode 100644 tests/fixtures/cpp_logger/a/Logger.cpp create mode 100644 tests/fixtures/cpp_logger/a/Logger.h create mode 100644 tests/fixtures/cpp_logger/b/Logger.cpp create mode 100644 tests/fixtures/cpp_logger/b/Logger.h create mode 100644 tests/fixtures/cpp_paired/Foo.cpp create mode 100644 tests/fixtures/cpp_paired/Foo.h create mode 100644 tests/fixtures/cpp_paired/Main.cpp create mode 100644 tests/fixtures/cpp_samedir/Alpha.h create mode 100644 tests/fixtures/cpp_samedir/Beta.h create mode 100644 tests/fixtures/cpp_samedir/plain.h create mode 100644 tests/fixtures/objc_mixed/Bridging-Header.h create mode 100644 tests/fixtures/objc_mixed/Widget.h create mode 100644 tests/fixtures/objc_mixed/Widget.m create mode 100644 tests/fixtures/objc_mixed/WidgetExtras.swift create mode 100644 tests/fixtures/pascal_cross_file/BaseGadget.pas create mode 100644 tests/fixtures/pascal_cross_file/DerivedGadget.pas create mode 100644 tests/fixtures/pascal_cross_file/OtherGadget.pas create mode 100644 tests/fixtures/sample_scoped_calls.pas create mode 100644 tests/test_affected_member_seed.py create mode 100644 tests/test_build_merge_hyperedges_and_prune.py create mode 100644 tests/test_builtin_global_type_refs.py create mode 100644 tests/test_case_sensitive_resolution.py create mode 100644 tests/test_cli_broken_pipe.py create mode 100644 tests/test_community_hub_labels.py create mode 100644 tests/test_corrupt_graph_json.py create mode 100644 tests/test_cpp_objc_cross_file_calls.py create mode 100644 tests/test_cross_language_call_resolution.py create mode 100644 tests/test_csharp_member_calls.py create mode 100644 tests/test_extract_cache_location.py create mode 100644 tests/test_extract_code_only_cli.py create mode 100644 tests/test_extractors_registry.py create mode 100644 tests/test_gemini_hook.py create mode 100644 tests/test_hook_guard.py create mode 100644 tests/test_indirect_dispatch.py create mode 100644 tests/test_indirect_dispatch_assign_return.py create mode 100644 tests/test_indirect_dispatch_getattr.py create mode 100644 tests/test_java_member_calls.py create mode 100644 tests/test_long_path_hashing.py create mode 100644 tests/test_merge_graphs_cli.py create mode 100644 tests/test_office_incremental.py create mode 100644 tests/test_ollama_retry_cap.py create mode 100644 tests/test_pascal_call_scoping.py create mode 100644 tests/test_pascal_resolution.py create mode 100644 tests/test_paths.py create mode 100644 tests/test_phantom_cross_package_call.py create mode 100644 tests/test_phantom_external_import.py create mode 100644 tests/test_replace_or_append_section.py create mode 100644 tests/test_search_hook.py create mode 100644 tests/test_semantic_fragment_sanitize.py create mode 100644 tests/test_semantic_id_remap_root.py create mode 100644 tests/test_skill_version_warning.py create mode 100644 tests/test_ts_decorators.py create mode 100644 tests/test_ts_generators.py create mode 100644 tests/test_ts_import_require.py create mode 100644 tests/test_ts_namespace.py create mode 100644 tests/test_ts_receiver_member_calls.py create mode 100644 tests/test_typescript_module_extensions.py create mode 100644 tests/test_word_count_cache.py create mode 100644 tests/test_zero_node_no_cache.py diff --git a/BENCHMARKS.md b/BENCHMARKS.md new file mode 100644 index 000000000..6c1a6d331 --- /dev/null +++ b/BENCHMARKS.md @@ -0,0 +1,187 @@ +# graphify Benchmarks + +How graphify performs as conversational long-term memory and as a +code-intelligence layer, measured on an open harness with competing systems run +under identical conditions (same model, same budgets, same grader). + +Last updated: 2026-07-05. + +## Summary + +graphify's deterministic graph plus hybrid retrieval has the best retrieval +recall on LOCOMO of any system tested, the best LOCOMO QA accuracy per dollar, +ties for the best LongMemEval score, and builds its index with zero LLM credits. +Every system was run on the same harness with one shared model (Kimi K2.6), +identical budgets, and a judge blind-validated against a second independent judge +(90.6% agreement, Cohen's kappa 0.81). + +Highlights: +- LOCOMO retrieval recall@10 of 0.497, about 10x mem0 (0.048) and above BM25 (0.362). +- LOCOMO QA accuracy of 45.3%: +18 points over mem0, +14 over BM25, and within + 4.4 points of supermemory at about a tenth of supermemory's ingest cost. +- LongMemEval-S of 76%, tied for best with dense RAG. +- Zero LLM credits to build the graph, and about 11x cheaper memory ingest than + supermemory ($1.40 vs $15.67). + +## Results at a glance + +| Suite | Dataset (n) | Metric | graphify | Field | +|---|---|---|---|---| +| Memory | LOCOMO (300) | QA accuracy | 45.3% | supermemory 49.7% (11x ingest cost), bm25 31.3%, mem0 27.3% | +| Memory | LOCOMO (300) | recall@10 | 0.497 | bm25 0.362, mem0 0.048 | +| Memory | LongMemEval-S (50) | QA accuracy | 76% | dense RAG 76%, hybrid 74%, mem0 70% | +| Cost | LOCOMO ingest | USD | ~$1.40 | supermemory $15.67, mem0 $3.48 | +| Cost | graph build | LLM credits | $0 | n/a | + +## Harness + +graphify's own harness. Competing systems (mem0, supermemory) are run as +adapters inside it, so every system sees the same model, token budget, and +grader. + +``` +ingest -> index -> search -> answer -> grade +(build) (store) (retrieve) (Kimi K2.6) (key-fact coverage) +``` + +- Memory suite (`memory/`): graphify's graph retrieval vs dedicated memory + systems (mem0, supermemory) and classic baselines (BM25, dense RAG, + hybrid RRF). mem0 and supermemory run self-hosted as adapters, wired through + a proxy so their LLM calls also use Kimi K2.6. +- Code suite (`crosstool/`): a fixed coding agent (Claude Opus 4.8, at most 14 + turns, a grep/read/list floor plus one code-intelligence tool) answers graded + questions on ERPNext, a roughly 1M-LOC production repo + ([frappe/erpnext](https://github.com/frappe/erpnext)), with a temporal + sub-suite of 689 weekly AST checkpoints from 2011 to 2026. + +## Datasets + +- LOCOMO (`locomo10.json`, n=300): multi-session conversational QA. +- LongMemEval-S (n=50, English subset): long-horizon conversational memory. +- ERPNext: a large real-world Python codebase for code intelligence. + +LOCOMO and LongMemEval are the same academic datasets other memory systems +report on, so results are cross-referenceable. Datasets are not redistributed; +the harness documents the expected local layout. + +## Judge and grading + +Answers are graded by Kimi K2.6 against a gold set of atomic key facts a correct +answer must contain: + +``` +coverage = (covered + 0.5 * partial) / total +``` + +Every verdict cites a verbatim quote from the answer, so grades are auditable +rather than one opaque score. + +Judge validation: the judge was blind-validated against a second, independent +judge on a sampled set at 90.6% agreement, Cohen's kappa 0.81 (substantial +agreement). Most published memory benchmarks disclose no judge validation at +all; we publish ours so the grading itself can be audited. + +## Fairness rules + +- One model for every LLM role: Kimi K2.6 via Moonshot. +- One shared local embedder where the system allows it: BGE-m3 (1024-d, + multilingual). +- Identical token budgets. Every run writes a spend ledger and respects + `--max-spend`. +- Graphs build AST-only with no LLM (an unset API key produces zero credits); + embeddings use a local deterministic model. + +## Results: conversational memory + +### LOCOMO (n=300) + +Sorted by recall@10. + +| System | QA accuracy | recall@10 | Ingest cost | +|---|---|---|---| +| **graphify** (graph-expand) | **45.3%** | **0.497** | ~$1.40 | +| hybrid RRF | 43.3% | 0.493 | $0 (shared index) | +| graphify (SurrealDB engine) | 43.3% | 0.485 | $0 (shared index) | +| dense RAG | 41.3% | 0.439 | $0 (shared index) | +| BM25 | 31.3% | 0.362 | $0 (shared index) | +| supermemory | 49.7% | 0.149* | $15.67 | +| mem0 | 27.3% | 0.048 | $3.48 | + +Bold marks graphify's primary configuration, not the column maximum. Baselines +retrieve from the same harness-built index, so they incur no separate ingest +cost. + +`*` Retrieval-recall is embedder-confounded: supermemory's self-host locks in +its own 768-d English-only embedder rather than the shared BGE-m3. The +QA-accuracy axis (a shared Kimi reader and judge over each system's hits) is the +clean comparison. + +Reading: supermemory scores a few points higher on raw QA, but at about 11x the +ingest cost ($15.67 vs $1.40) and with about 3x worse retrieval recall. graphify +has the best retrieval recall on LOCOMO of any system tested, the best QA of the +systems on the shared embedder, and does it for about a tenth of supermemory's +cost. It retrieves the right memory about 10x more often than mem0 and answers ++18 points more accurately. A seed-only ablation (no graph expansion) still +scores 42.7% at $1.40 ingest, so most of the accuracy holds at the cheapest +setting. + +### LongMemEval-S (n=50) + +| System | QA accuracy | recall@10 | +|---|---|---| +| **graphify** (graph-expand) | **76%** | **0.844** | +| dense RAG | 76% | 0.848 | +| graphify (SurrealDB engine) | 74% | 0.833 | +| hybrid RRF | 74% | 0.822 | +| BM25 | 70% | 0.710 | +| mem0 | 70% | 0.344 | + +graphify ties dense RAG for the best QA accuracy (76%); dense RAG edges it on +recall (0.848 vs 0.844). Both retrieve far more than mem0 (recall 0.344). + +## Results: code intelligence + +On ERPNext (a roughly 1M-LOC production repo), giving a fixed coding agent one +graphify tool lifts key-fact coverage across the graded question set (n=6) from +70.8% (a grep and read baseline) to 82.0%, at about 140K tokens per query. +graphify pays for itself in accuracy against searching raw files, and avoids the +context-stuffing anti-pattern of packing the whole repo into every turn (which +costs roughly 20x the tokens for lower coverage). + +## Results: temporal (15 years of ERPNext) + +689 weekly AST checkpoints, 2011 to 2026, built deterministically with no LLM. + +| Checkpoint | Nodes | Edges | Files | +|---|---|---|---| +| 2011-06-08 | 3,069 | 2,900 | 1,032 | +| 2026-06-24 | 22,620 | 48,710 | 3,758 | + +The graph grows about 7x in nodes and 17x in edges across the span. As the +codebase grows, plain lexical retrieval finds less of the answer while graph and +semantic retrieval scale with it, and the AST extraction itself stays stable. + +## Cost and token economics + +- Graph construction costs zero LLM credits. graphify extracts with tree-sitter + (deterministic, about 40 languages) and a local embedder, so building the + index uses no API tokens. Most memory and semantic-retrieval systems pay a + per-document LLM ingest cost. +- Memory ingest is about 11x cheaper: graphify's LOCOMO ingest runs around + $1.40 against supermemory's $15.67. +- Every number here is backed by a per-run spend ledger in the harness output. + +## Reproducing + +Set `MOONSHOT_API_KEY`. Datasets are fetched to the local layout documented in +the harness. Each run respects `--max-spend` and writes a spend ledger. + +```bash +# Memory (LOCOMO). This invokes the SurrealDB-engine row (43.3%); the +# graph-expand headline (45.3%) is a separate adapter in the same harness. +python memory/runner.py --phase 3 --split locomo --n 300 \ + --adapters graphify_v1_surreal --cn natural --workers 6 --max-spend 15 + +# Code cross-tool (ERPNext) +python crosstool/run.py --repo erpnext --max-spend +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index f2bf99e92..5bc971b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,212 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) +## 0.9.14 (2026-07-12) + +- Fix: Visual Studio *solution folder* nodes no longer embed the absolute scan path (including the local username) in their `id` and `source_file` (#1789, thanks @fremat79). A solution folder is a virtual grouping, not a file — VS writes its name as both the display name and the "path" — but `extract_sln` resolved it to an absolute filesystem path anyway and keyed the node id off that. The CLI's id-relativization pass only remaps ids of real files in the scan set, so a virtual folder never matched and its absolute id survived into a committed `graph.json` (e.g. `id=/Users//proj/Plugins` instead of `id=plugins`). Solution folders are now detected (name == path) and keyed off the folder name only; real project files still resolve as before. (The earlier fix covered `.csproj`/`.sln` file nodes but missed the virtual folders — this completes it.) + +- Fix: the CLI no longer crashes with exit code 255 when a downstream reader closes the pipe early (#1807 / #1811, thanks @varuntej07). Truncating output with `head`, PowerShell's `Select-Object -First N`, or `sed q` disconnected the reader mid-write, graphify hit an unhandled `BrokenPipeError` (or `OSError(EINVAL)` on Windows) and exited 255 — so CI wrappers and agent harnesses that both trim output and check the exit code read a successful query as a command failure. An early-closing reader is now treated as success: stdout is flushed inside the guard (piped stdout is block-buffered, so a small output would otherwise only flush at interpreter shutdown, where the error escapes as a noisy "Exception ignored" and a nonzero exit), then redirected to devnull so the shutdown flush can't raise again, and the process exits 0. + +- Fix: `extract()` no longer writes its AST cache into the analyzed source tree (#1774 / #1802, thanks @SimiSips). With no explicit `cache_root`, the cache defaulted to the inferred common parent of the input files — the source tree — so analyzing a read-only or foreign corpus silently created `graphify-out/cache/` inside it. The cache is an output, so it now defaults to the current working directory. Crucially, the cache *location* is decoupled from the key/id *anchor*: the inferred common parent still anchors the content-hash keys, node ids, symbol resolution, and the XAML/C# project-scan boundary, so keys stay relative and portable (shared/CI cache reuse keeps working) and out-of-CWD corpora aren't mis-scanned. An explicit `cache_root` (as the CLI and watcher pass) is unchanged. + +- Fix: `graphify query` no longer floods results with homonymous generic symbols (#1766 / #1832, thanks @devcool20). When dozens of nodes share one generic label — framework route handlers all labelled `GET`/`POST`, a repeated `handler` — they used to consume every BFS seed slot, so the traversal explored many near-identical neighborhoods and drowned out the query's actual target. Seed selection now deduplicates by normalized label (`GET`/`Get`/`get` collapse together), keeping at most one representative per label while still guaranteeing a seed for each distinct query term. The per-label cap is scoping-only — the shared node scorer (also used by `shortest_path`/`explain` endpoint resolution) is left untouched, so those are unaffected. + +- Fix: semantic cache writes are now scoped to the files actually dispatched in each extraction batch (#1757 / #1835, thanks @TPAteeq). A model can attribute a node's `source_file` to another corpus file; `save_semantic_cache` would then replace (or, mid-run, pollute) that other file's complete cache entry with a stray fragment — silently, with no shrink guard. Writes now honor an `allowed_source_files` allowlist: the final CLI write is scoped to the uncached file set, and the per-chunk incremental checkpoint (`extract_corpus_parallel`, the default path for `extract`/`update`) is scoped to that chunk's own files, so an out-of-scope attribution is skipped with a warning instead of clobbering a legitimate entry. + +- Fix: `graphify export graphml` no longer crashes on dict- or list-valued attributes (#1831 / #1830, thanks @hofmockel). `nx.write_graphml` only accepts scalar values, so a per-node `metadata` dict or the graph-level `hyperedges` list raised `GraphML does not support type ` and failed the entire export — on a real ~2,300-node graph, every attempt. `to_graphml` now coerces `None -> ""` and JSON-serializes non-scalars across graph/node/edge scopes (GraphML-native int/float/bool/str pass through unchanged), and writes atomically via a temp file so a failed export no longer leaves a 0-byte `.graphml` that downstream tooling mistakes for a completed one. + +- Fix: `.nox/` (nox virtualenvs) is now skipped during detection alongside `.tox/` (#1804, thanks @igorregoir-lgtm). nox is tox's successor and creates a `.nox/` tree of the same shape, but only `.tox` was in the skip set — so a repo with a nox env got its site-packages fully indexed (one real repo came out 91% venv noise: 6,720 of 7,365 nodes from `.nox/`) and semantic extraction burned tokens reading venv docs. + +- Fix: detection now honors `.git/info/exclude`, not just `.gitignore`/`.graphifyignore` (#1810, thanks @cdahl86-cyber). `info/exclude` is where git records local-only excludes and where `git worktree add` writes nested worktree paths, so a repo can exclude a directory without any `.gitignore` entry. graphify walked straight into those worktree copies and the graph exploded (one repo with 5 worktrees went from ~9,400 nodes / 10 MB to ~210,000 nodes / 311 MB, ~77% duplicate worktree nodes, near the 512 MB cap — regenerated on every commit by the auto-rebuild hooks). The exclude file is loaded at lowest precedence (below every per-directory `.gitignore`/`.graphifyignore`), matching git, so a nearer `!` re-include still wins; the linked-worktree/submodule case (where `.git` is a file) is resolved to the shared common git dir. + +- Fix: the git hooks no longer misbehave inside linked worktrees, and `GRAPHIFY_SKIP_HOOK` now suppresses both hooks (#1809, thanks @cdahl86-cyber; worktree guard co-developed with @Claude-Madera's #1806). Two gaps: (1) `post-checkout` never checked `GRAPHIFY_SKIP_HOOK`, so the var stopped commit-triggered rebuilds but not branch-switch ones; it now honors it like `post-commit`. (2) With `core.hooksPath` shared across worktrees, a commit in any linked worktree fired `post-commit`, which wrote a rogue delta-only `graph.json` into that worktree and raced deploy/CI `git clean` against the detached rebuild (`failed to remove graphify-out/: Directory not empty`). Both hooks now short-circuit in a linked worktree (git-dir != git-common-dir), comparing absolute paths so the primary checkout — where `--git-common-dir` is the relative `.git` — is never false-positived and wrongly skipped. + +## 0.9.13 (2026-07-12) + +- Fix: the query log is now opt-in (off by default) (#1797, thanks @adam-pond-agent). `querylog` wrote every `query`/`path`/`explain` question and corpus path (and full responses if `GRAPHIFY_QUERY_LOG_RESPONSES`) to a default-on, unbounded, fail-silent plaintext file at `~/.cache/graphify-queries.log` — outside any repo's .gitignore/retention, and undocumented, which contradicts graphify's on-device / no-telemetry posture. Logging is now OFF unless you opt in with `GRAPHIFY_QUERY_LOG_ENABLE=1` (default path) or `GRAPHIFY_QUERY_LOG=`; `GRAPHIFY_QUERY_LOG_DISABLE=1` still forces it off. All the query-log env vars are now documented in the README. + +- Fix: a markdown file that went through semantic extraction is no longer duplicated into two disconnected nodes on later `graphify update` (#1799, thanks @jerp86). The semantic pass mints `_doc` while the markdown quick-scan mints the bare ``, so the file's edges split across two twins (a docs->code path query would dead-end on the bare half; centrality and communities split too). `build_from_json` now merges the bare quick-scan node into the semantic `_doc` node when both share the same `source_file` and are `file_type: document`, consolidating their edges/hyperedges onto one node. Gated so an unrelated code symbol `foo` and `foo_doc` never merge. + +- Fix: incremental `graphify update` no longer silently evicts nodes for a file that left the scan corpus but still exists on disk (#1795, thanks @CJNA). `_reconcile_existing_graph` read "source absent from the collected corpus" as "deleted", but that's also what an ignore-rule/filter change looks like (e.g. an upgrade that starts honoring `.gitignore`) — in one 27k-node graph the first rebuild after such an upgrade mass-evicted 655 nodes whose files were present the whole time. Eviction now fails closed: a corpus-absent source is only evicted when `Path(identity).exists()` is False (true deletion), otherwise its nodes/edges/hyperedges are preserved and a loud line reports how many were kept and why. True deletions and renames evict as before; a full `extract --force` still purges deliberate exclusions. + +- Fix: `build_merge` no longer silently deletes a re-extracted file's fresh nodes when that file is also passed in `prune_sources` (#1796, thanks @erichkusuki). A file present in `new_chunks` is being replaced, not deleted, so it's now excluded from the prune set — "replace" wins over a contradictory "delete" of the same source. Previously, following the old edit-workflow (pass the changed file in `prune_sources`) deleted the just-built concept whenever an edit kept a node's label. Genuine deletions (a file in `prune_sources` but not `new_chunks`) still prune. + +- Fix: `graphify path` resolves each endpoint to the first candidate whose label contains every query token, instead of blindly taking the top-scored node (#1785, thanks @CJNA). `_score_nodes`' full-query bonus only fires when the query equals/prefixes a label, so a query that is a token *subset* of the intended label (`"Reject-everything judge"` vs `"Degenerate Reject-Everything Judge"`) got no bonus and a node prefix-matching one rare token could outscore it — anchoring the path on an unrelated, often disconnected node and yielding a false "No path found". When the top candidate already full-matches (the common case) the pick is unchanged. Applied to both the `path` CLI and the MCP shortest-path tool; the close-runner-up ambiguity warning now fires only when the score head is what was actually picked. + +- Fix: the report's "Suggested Questions" weakly-connected-node count now matches its "Knowledge Gaps" count (#1768, thanks @balloon72). `suggest_questions()` omitted the `file_type != "rationale"` filter that `report.py`'s Knowledge Gaps section applies, so the same `GRAPH_REPORT.md` showed two different numbers for the same concept (e.g. 757 vs 245), making a healthy graph look like it had a major documentation gap. Both computations now use the same filter. + +- Fix: Bash scripts that run each other by execution now get a cross-file edge (#1756, thanks @balloon72). `extract_bash` only linked `source x.sh` / `. x.sh`; the two most common forms — `bash x.sh` and `./x.sh` — produced no edge, so execution topology was missing. They now emit a `calls` edge (context `script_invocation`) to the invoked script's entry node when the target resolves to a real file on disk (script runners `bash`/`sh`/`zsh`/`ksh`/`dash` and bare `./x.sh`), skipping missing or shadowed targets. + +- Fix: Ruby `.rake` files are now extracted and participate in Ruby cross-file resolution like `.rb` (#1784, thanks @krishnateja7). `.rake` is plain Ruby but the extension was gated out of seven places (classification, extractor dispatch, the language-name/family maps, the `ruby_member_calls` resolver's suffix set, both `.rb`-suffix filters in `ruby_resolution.py`, and the build repo-tag map), so every rake task was skipped and its calls were invisible. All seven now include `.rake`; `Widget.tally` from a `.rake` task resolves to its `.rb` definition. + +- Fix: cross-module references to a function now resolve to its definition instead of dangling on a name-only stub (#1781, thanks @EmilNyg). `_rewire_unique_stub_nodes` gated merge targets through `_is_type_like_definition`, which rejects any label ending in `)` — so function/method defs could never absorb their reference stubs, and "who references this function" returned nothing on the definition node while a sourceless stub held all the edges. Top-level function defs are now eligible rewire targets when the label match is globally unique, gated by a language-family match with the referrers (a Python `get_db` reference can't bind to a unique Go `get_db()`) and excluding stubs used as a supertype (`inherits`/`implements`/`extends` — you don't inherit from a function). Types are unchanged. + +## 0.9.12 (2026-07-10) + +- Fix: live PostgreSQL introspection (`--postgres`) now emits foreign-key `references` edges under a read-only role (#1746, thanks @rithyKabir). The FK query read `information_schema.referential_constraints`, which is privilege-filtered — a role with only SELECT sees zero FK rows while tables/views/routines still appear, so every `references` edge silently vanished. It now reads the world-readable `pg_catalog.pg_constraint` (keyed by oid, which also fixes same-named constraints on sibling tables cross-matching in the old name-based joins), preserving composite-FK column order via `UNNEST ... WITH ORDINALITY`. + +- Fix: `json_config` no longer emits `imports`/`extends` edges to node IDs it never creates (#1764, thanks @oleksii-tumanov). `package.json` dependencies and `tsconfig.json` `extends`/`$ref` targets produced edges whose endpoint node was absent, so `build_from_json` silently dropped them (the "no matching node id" case is filtered out of real errors) — losing dependency/extends structure on two of the most common files in any JS/TS repo. The extractor now creates the referenced target as a `concept` node before adding the edge. + +- Fix: `graphify update` no longer deletes semantic hyperedges on every run (#1755, thanks @oleksii-tumanov). The AST-only rebuild treated every rebuilt corpus file as grounds to evict hyperedges anchored to it, but the AST pass never re-emits hyperedges, so doc-sourced hyperedges (exactly what semantic extraction produces) were permanently lost on the first `update` after a full build — even a no-op run. Hyperedge eviction is now scoped to genuinely deleted (or symlink-outside) sources, mirroring node/edge handling; replacement-by-id and dangling-member cleanup are unchanged. + +- Fix: Java member calls resolve against the receiver's declared type instead of a bare method-name match (#1696/#1697, thanks @oleksii-tumanov). `gw.charge()` where `gw: PaymentGateway` now binds to `PaymentGateway.charge`, not a same-named `AuditLog.charge` in another file. Explicit-type receivers and `this` are exact; current-class fields, method parameters, and explicitly-typed locals resolve via a method-scoped type table; a missing, ambiguous, inherited, or chained receiver is skipped rather than guessed (same god-node guard as the C#/Swift/Ruby resolvers). Fully-qualified and nested-type receivers are deferred (they need package/nesting-aware type identity). + +- Fix: output/cache artifacts no longer land in the scanned corpus or CWD when `--out`/`--graph` point elsewhere (#1747, thanks @bbqboogiedwonsen). `extract --out ` correctly wrote the graph to `` but `detect()`'s word-count/stat-index cache still created a stray `graphify-out/cache/` inside the corpus (it uses the scan root); it now honors the `--out` dir via a threaded `cache_root`. And `cluster-only --graph /graphify-out/graph.json` wrote `GRAPH_REPORT.md`/labels/analysis/re-clustered graph to the CWD instead of beside the input; it now writes beside `--graph` when that graph lives in a `graphify-out/` dir, while still restoring into the CWD for an archived `backup/graph.json` (#934). + +- Fix: `imports`/`references` edges no longer bind across a language boundary (#1749, thanks @philberndt). The spec already forbids cross-language `calls`, but an unresolved Python `import time` could still resolve by bare stem onto a `src/time.ts` file node — welding a polyglot repo's halves together at a phantom edge (in the reporter's repo, 3 such edges were the *only* thing bridging 2409 Python nodes to 1403 TS nodes, inflating `time.ts` betweenness ~90x and making it the #1 "god node"). The build-time cross-language guard now covers `imports`/`imports_from`/`references` in addition to `calls`, dropping an edge only when both endpoints are known code languages of different interop families (so a config/manifest → code reference is untouched). + +- Fix: files whose extractor bailed out for a missing optional dependency no longer vanish without a trace (#1745, thanks @rithyKabir). `.sql` files (and other extra-gated languages) have a dispatch entry, so the #1689 no-extractor warning can't fire, and `extract_sql` returns an error result when `tree-sitter-sql` is absent, so the #1666 zero-node warning skips it too — the graph built "successfully" while an entire SQL corpus contributed nothing. `extract()` now surfaces these grouped by extension, naming the extra that restores the language (e.g. `pip install "graphifyy[sql]"`). + +- Fix: `build_from_json` is deterministic across process runs again (#1753, thanks @erasmust-dotcom). The ghost-node merge iterated `set(G.nodes())`, so which node survived a `(basename, label)` collision depended on CPython's per-process string-hash seed — rebuilding the same extraction JSON in a fresh process could silently pick a different canonical id (breaking the cluster→relabel workflow with a `KeyError` on an id that vanished). The Pass 1/Pass 2 loops now iterate in sorted order. Additionally, two non-AST (semantic) nodes sharing a key but from *different* files are now treated as distinct concepts and both survive (mirroring the AST/AST ambiguity guard #1257) instead of one arbitrarily merging away; a genuine same-file duplicate still collapses. + +- Fix: a Java field/parameter/return-type reference to a class whose simple name is shared by two modules no longer dangles on a sourceless phantom node (#1744, thanks @aviciot). Both same-named classes already survive as distinct path-scoped nodes, but the cross-module `references` edge was left pointing at a bare no-source stub because `_resolve_java_type_references` re-pointed `implements`/`inherits`/`imports` but not `references` — so a query about the referenced class could miss it. The Java resolver now disambiguates `references` by the importing file's `import` statement (falling back to same-package), mirroring the C# resolver, and drops the orphaned phantom. + +## 0.9.11 (2026-07-08) + +- Fix: file enumeration no longer silently drops a directory subtree. `detect()`'s `os.walk` had no `onerror` handler, so an `os.scandir` failure (a permission error, or a directory created/deleted mid-walk by concurrent writes) was swallowed and that whole subtree vanished from the scan with no log, yielding a silently partial `graph.json`. The walk now records every skipped directory (surfaced in the result's `walk_errors`) and warns to stderr, while still enumerating the rest. Relatedly, `to_json`'s anti-shrink guard (#479) now fails safe: a non-empty but unreadable existing `graph.json` refuses the overwrite (pass `force=True` to override) instead of silently clobbering a good graph; an empty file still proceeds. +- Fix: Pascal/Delphi extractors no longer emit duplicate `method`/`contains`/`inherits` edges. A class method declared in the interface section and defined in the implementation section each emitted an edge to the same node, so ~half of a Pascal graph's method edges were doubled (skewing degree/centrality and tripping the new cross-file resolver's god-node guard). Both extractors now dedup edges on (source, target, relation), mirroring the existing node dedup. +- Fix: Pascal/Delphi call resolution is scoped to the caller's class + inherits chain, and calls to methods inherited across file boundaries now resolve (#1739, thanks @richtext). Both extractors previously resolved every call via a single file-wide `{name: node_id}` dict, so two unrelated classes with a same-named method (property accessors, generated COM/TLB wrappers) collapsed onto whichever was inserted last, producing wrong cross-class `calls` edges. Resolution now walks own-class then ancestor chain then file-level free functions, emitting no edge when ambiguous (same god-node guard as the Ruby resolver). A new corpus-wide resolver (`graphify/pascal_resolution.py`) resolves calls from a descendant to a base-class method declared in a different file (the common generated-base/manual-descendant split). Also stops emitting a duplicate cross-file base-class stub carrying the wrong `source_file`. +- Fix: query ranking no longer lets a lone generic term that exact-matches a short leaf label hijack seed selection in multi-term queries (#1602/#1724, thanks @fkhawajagh). `_score_nodes` scales the per-term exact/prefix tiers by squared term coverage; single-term and full-coverage queries are unchanged. +- Fix: Kotlin enum entries are extracted as nodes with `case_of` edges to their enum (#1700, thanks @ivanzhilovich). Closes the Kotlin half of #1700 (the Java half shipped in 0.9.10 via #1719); `enum class ChatType { NORMAL, GROUP, SYSTEM }` now yields NORMAL/GROUP/SYSTEM nodes and "where is ChatType.X used" works for Kotlin. +- Fix: SKILL.md's POSIX interpreter probe no longer silently falls back to a graphify-less system python (#1735, thanks @mohammedMsgm). Step 1 ran `uv tool run graphifyy python -c ...`, but the `graphifyy` package's executable is `graphify`, so uv treated `python` as a missing `graphifyy` command; `2>/dev/null` hid uv's own `--from` hint, leaving `PYTHON` on an interpreter without graphify. The probe now runs `uv tool run --from graphifyy python -c ...`. The PowerShell path was already correct. + +- Refactor: decomposed the two largest modules into focused, single-responsibility modules — verbatim moves only, every original import path preserved via re-exports, no behavior change (#1737, thanks @TPAteeq). `extract.py` 17,054 → 4,740 LOC (the tree-sitter engine, cross-file resolution, shared models, and 23 language extractors moved under `graphify/extractors/`), `__main__.py` 5,368 → 673 (install/uninstall + CLI dispatch split into `graphify/install.py` and `graphify/cli.py`), `export.py` 1,671 → 962 (HTML + graph-DB exporters under `graphify/exporters/`). Full suite unchanged. +- Fix: `merge-graphs` gives each input a distinct repo tag so same-stem nodes from different source graphs don't collapse (#1729). Two graphs under a same-named repo dir (`src/graphify-out` and `frontend/src/graphify-out`, both → `src`) shared the `src::` prefix, so a backend `src/app.js` and a frontend `App.jsx` (both bare `app`) merged into one node with edges from both — false cross-runtime `path` results. Colliding tags are now widened (`frontend_src`) with an index-suffix backstop, and the command prints a note when it disambiguates. +- Fix: `uninstall` removes the graphify hook/section from Claude's local-only files too (#1731, thanks @TPAteeq). It now cleans `.claude/settings.local.json` and both `CLAUDE.local.md` locations in addition to the standard files, via both `graphify uninstall` and `graphify claude uninstall`. +- Feat: `graphify extract --code-only` indexes code (local AST, no API key) and skips the doc/paper/image semantic pass, so a mixed repo no longer hard-fails when no LLM backend is configured (#1734). Reports what it skipped; the no-key error now points users at the flag. + +## 0.9.10 (2026-07-08) + +- Fix: TS/JS member calls on a builtin-typed receiver no longer collapse onto a same-named user symbol (#1726). `_resolve_typescript_member_calls` matched a receiver's type to a definition by casefolded label, so `x: Date; x.getTime()` bound the caller to a user `class DATE`/`const DATE` in another file — inventing hundreds of phantom `references` edges and a false god node. Builtin-global receiver types (`Date`, `Promise`, `Map`, ...) are now skipped, mirroring the cross-file call guard; genuine user types are unaffected. +- Fix: never bind a cross-file `calls` edge to a definition in a different language family (#1718, thanks @edinaldoof). Name-only matching resolved a TSX callback passed by name to a same-named Kotlin method (and a Python call to a Kotlin fun) — phantom edges the spec forbids. Candidates are now filtered by interop family (JVM, native C-family, JS/TS module graph, ...); unknown families stay permissive. +- Fix: an ambiguous legacy-stem alias in `build_merge` no longer silently merges two unrelated files (#1713, thanks @mallyskies). The `#1504` old-stem alias (`ping.h`/`ping.php` → bare `ping`) resolved by hash-order, riding a dangling edge onto an arbitrary same-named file. Aliases are now committed only when exactly one file claims them; a salted `.h`/`.cpp` file node is recognized as its own claimant so a genuine collision stays ambiguous (and dropped) instead of picking a wrong winner. +- Fix: inline base-class stubs are tagged with `origin_file` (#1707, thanks @mallyskies). Five inheritance handlers built cross-file base-class stubs without `origin_file`, so same-named bases across files collapsed onto one shared stub that could then merge with an unrelated real class (218 wrong `inherits` edges observed). They now route through `ensure_named_node`, which sets the tag. +- Fix: Java enum constants are extracted as nodes with `case_of` edges to their enum (#1719, thanks @ivanzhl). Closes the Java half of #1700; `affected ErrorCode` / "where is ErrorCode.X used" now works for Java. +- Fix: `graphify` rebuilds recover from a deleted hook working directory instead of crashing (#1703, thanks @FranciscoJSBarragan). A detached git hook can inherit a CWD that no longer exists; the rebuild now recovers via `GRAPHIFY_REPO_ROOT` or fails cleanly instead of raising `FileNotFoundError`. +- Feat: the semantic cache is checkpointed per chunk so an interrupted extraction resumes instead of restarting (#1715, thanks @A-Levin). Each completed chunk is unioned into the cache immediately (opt out with `GRAPHIFY_NO_INCREMENTAL_CACHE`); the final write still overwrites authoritatively. +- Docs: `SECURITY.md` no longer claims stdio-only now that an opt-in `--transport http` (binds `127.0.0.1` by default) exists (#1714, thanks @Thizeidler); added tests for `GRAPHIFY_MAX_GRAPH_BYTES` parsing and corrected its unit docstring to binary MiB/GiB (#1722, thanks @Cekaru). + +## 0.9.9 (2026-07-07) + +- Fix: `graphify explain` resolves an exactly-typed punctuated label symmetrically against `norm_label` (#1704). The search term tokenized on `\w+` ("blockStream.ts" -> "blockstream ts", space where the '.' was) while a node's stored `norm_label` keeps punctuation ("blockstream.ts"). The verbatim case was already rescued by the tokenized-label tier, but that broke if a node's `label` and `norm_label` diverged; a punctuation-preserving `norm_query` is now matched against `norm_label` across the exact/prefix/substring tiers (and fed to the trigram prefilter), so it is robust by construction. +- Fix: code files with no AST extractor are surfaced instead of silently dropped (#1689, thanks for the precise root-cause). `.r`/`.R` (also `.ejs`, `.ets`) are in `CODE_EXTENSIONS` so they are counted as code, but there is no extractor for them, so they produced zero nodes with no warning. `extract` now prints a grouped warning ("N file(s) are classified as code but graphify has no AST extractor ...: .r (17)"). Adding a real `tree-sitter-r` extractor remains a follow-up. +- Fix: the AST-extraction progress line keeps a consistent denominator to the end (#1693). Intermediate lines counted against `len(uncached_work)` but the final line switched to `total_files` (which includes cached hits and no-extractor files), so on a large corpus the count appeared to jump upward right after 99%. Both the parallel and sequential final lines now use the `uncached_work` denominator. +- Fix: `GRAPH_REPORT.md` no longer emits dangling `[[_COMMUNITY_*]]` Obsidian wikilinks by default (#1712). The `_COMMUNITY_*.md` notes those links target are only created by the opt-in `--obsidian` export, and the report is written at build time before any export, so on a default run every link dangled (spawning phantom nodes in a vault's graph view, literal brackets elsewhere). The Community Hubs section now renders as plain text by default; the wikilink form is behind an `obsidian=True` opt-in. +- Fix: `.m` files are no longer force-parsed by the Objective-C grammar when they are MATLAB (#1702, thanks @catalystdream for the diagnosis). `.m` is shared by Objective-C and MATLAB, but the dispatch routed every `.m` to `extract_objc`, which turned real MATLAB into garbage nodes/edges. `.m` is now content-sniffed like `.h`: a genuine Objective-C `.m` (with `@implementation`/`@interface`/`@import`/`#import`) still routes to `extract_objc`; a MATLAB `.m` gets no extractor and is surfaced by the #1689 warning rather than mis-parsed. `.mm` is unchanged (unambiguously Objective-C++). A real `tree-sitter-matlab` extractor remains a follow-up. +- Fix: the `/graphify` usage comment in the skill files no longer claims a bare `/graphify` produces an Obsidian vault by default (#1681, thanks for the audit). It now reads "full pipeline on current directory (HTML viz; add `--obsidian` for a vault)", matching Step 6. Fixed at the skillgen source so every generated `skill-*.md` variant carries the corrected comment. +- Feat: files graphify sees but cannot classify are surfaced instead of vanishing (#1692). Extensionless, non-shebang project files (Dockerfile, Gemfile, Makefile, Rakefile, LICENSE, ...) and unsupported extensions previously left no trace at all. `detect` now collects them into an `unclassified` list, and `graphify extract` reports "N file(s) not classified (no supported extension or shebang), skipped: ...". Actually extracting Dockerfile/Makefile-style content remains a follow-up. + +## 0.9.8 (2026-07-06) + +- Fix: the Claude Code / Codebuddy `PreToolUse` and Gemini CLI `BeforeTool` graph-nudge hooks now work on Windows (#522). The hooks were inline POSIX bash (`case/esac`, `[ -f ]`, single-quoted `echo`), which Windows cmd.exe/PowerShell cannot parse — so on Windows the hook failed silently, no "run `graphify query` before grepping/reading raw files" context was injected, and users had to invoke `/graphify` by hand. The detection logic (grep-command match, source-file extension match, skip-if-under-output-dir, graph-exists check) moved into a shell-agnostic `graphify hook-guard ` subcommand invoked via the absolute exe path (the same pattern the codex hook already uses), so the hook parses and runs identically on Windows, macOS, and Linux. Behavior on macOS/Linux is unchanged (byte-identical nudge payload); the graph path now also honors `GRAPHIFY_OUT`. The Gemini `BeforeTool` hook got the same treatment (`graphify hook-guard gemini`), which also removes its dependency on a bare `python` being on PATH. Codex stays a no-op there because Codex Desktop rejects `additionalContext`. +- Fix: `--update`-style section writes to `CLAUDE.md`/`AGENTS.md` no longer corrupt or drop content (#1688, thanks @bdfinst). `_replace_or_append_section` located its managed block by substring (`marker in content`) and `next(... if marker in line)`, so a heading that appeared as a substring of another line (or duplicate headings) matched the wrong offset and the rewrite could truncate the file. It now matches the section heading exactly (`line.strip() == marker`), appends when absent, and prefers the last exact match when several exist, so unrelated content is preserved. +- Fix: token estimation no longer crashes on files containing tiktoken special-token text like `<|endoftext|>` (#1685, thanks @Kyzcreig). `_TOKENIZER.encode(content)` raises `ValueError` by default when the text contains a special token, which aborted packing on docs/corpora that merely mention these strings. Both `encode` sites now pass `disallowed_special=()` so such text is tokenized as ordinary bytes. +- Fix: the Ollama backend no longer multiplies a hang by the retry count (#1686, thanks @Kyzcreig). A stalled local model would wedge for `timeout * (max_retries + 1)`, which with the default 6 retries turned one long stall into a very long one. Ollama now defaults to zero client-side retries (a local model that stalls will not un-stall on retry); set `GRAPHIFY_MAX_RETRIES` to opt back in. Other backends are unchanged. Note: the underlying stall is non-deterministic and driven by the model server, so this bounds the wait rather than eliminating the hang. +- Fix: a truncated or slightly malformed community-labeling reply no longer discards the whole batch (#1690, thanks @vdgbcrypto). `_parse_label_response` now salvages the complete `"id": "name"` pairs from a reply that failed a strict `json.loads` (e.g. a reply truncated mid-object), raising only when no pairs can be recovered. The per-batch token budget was also raised (`256 + 48*n`, was `64 + 24*n`) to give models that prepend a short preamble enough headroom to finish the JSON. The exact provider truncation in the report could not be reproduced without a live key; the parser and budget fixes address the mechanism. +- Fix: cluster-only mode now reports the real token cost of community labeling instead of a hardcoded zero (#1694, thanks @sub4biz). The labeling LLM calls were never accounted for, so `GRAPH_REPORT.md`'s "Token cost" line always read `0 input · 0 output` in cluster-only runs. `_call_llm` now accumulates per-response usage into an optional accumulator that is threaded through the labeling path and surfaced in the report. Backends that do not return usage (the Claude Code CLI) still contribute nothing, which is honest rather than estimated. +- Docs/Feat: `deepseek-v4-flash` (and `v4-pro`) have thinking ENABLED by default; graphify no longer implies otherwise and adds an opt-in `GRAPHIFY_DISABLE_THINKING=1` toggle (#1621, thanks @sub4biz for the empirical testing). Disabling thinking removes a rare reasoning-leak failure mode (which the adaptive extraction/labeling retry already recovers from) but, measured on real corpora, trades it for more frequent benign truncation and measurably lower extraction quality and file coverage — so it stays a documented user choice rather than a forced default. The stale "non-thinking" comment on the built-in deepseek config is corrected. The moonshot (kimi) branch is unchanged (it must disable thinking or content comes back empty). +- Fix: source files are no longer silently dropped during discovery by two over-broad filters (#1666, thanks @krishnateja7 for the precise root-cause). (a) A bare `snapshots/` directory was pruned as a Jest/Vitest artifact, which killed legitimate code namespaces like a Rails `app/services/snapshots/`; it is now pruned only when it actually contains `.snap` files or sits directly under a JS test root (`__snapshots__` stays unconditionally pruned). (b) `_is_sensitive` dropped files on a bare name-keyword hit (`device_token.rb`, `passwords_controller.rb`) even when `classify_file` had already resolved them to source code; a genuine programming-language source file is now exempt from the weak keyword heuristic, while real secret stores in data/config formats (`credentials.json`, `secrets.yaml`, `.env`, `.pem`, ...) are still caught. This is the discovery-layer fix; the 0.9.7 no-cache-on-empty change could not surface these because the files never reached extraction. + +## 0.9.7 (2026-07-06) + +- Fix: Java standard-library types are no longer emitted as `references` noise (#1603, thanks @NydiaChung). A `_JAVA_BUILTIN_TYPES` skip list now suppresses ubiquitous `java.lang`/`java.util`/`java.io`/`java.time`/`java.math`/`java.nio.file` type names (`String`, `List`, `Map`, `Optional`, `Integer`, `Exception`, ...) at the type-ref walker; they never resolve to a project node, so edges to them were pure noise (mirrors `_GO_PREDECLARED_TYPES`/`_PYTHON_ANNOTATION_NOISE`). Nested user-type generic arguments still resolve: `List` drops the `List` edge but keeps `Item`. +- Feat: added a `pascal` optional extra for AST-quality Pascal/Delphi extraction (#1616, thanks @vinicius-l-machado). `extract_pascal` already used tree-sitter-pascal when present (with a regex fallback), but the grammar was never declared in the package metadata, so the AST path never ran out of the box. `uv tool install "graphifyy[pascal]"` now opts into it (also included in `[all]`); tree-sitter-pascal ships prebuilt wheels for every platform, so no C toolchain is needed. On a mid-size Delphi codebase the AST path yields notably more accurate `calls`/`inherits` edges than the regex fallback. +- Feat: JS/TS rationale comments and ADR/RFC citations are now extracted (#1599, thanks @niltonmourafilho-arch). Python already turned `# NOTE:`/`# WHY:`/`# HACK:` comments and docstrings into `rationale` nodes, but JS/TS comments were discarded. `extract_js` now runs the same post-pass: `// NOTE:`-style and block-comment rationale become `rationale` nodes with `rationale_for` edges, and `ADR-NNNN` / `RFC NNNN` citations become `doc_ref` nodes with `cites` edges from the file, closing the code-to-design-doc gap in mixed corpora at zero LLM cost (pure line scan). Files with no such comments are unaffected. +- Fix: extensionless executables with a shebang (CLI entry points like `devctl`, `manage`) are now extracted (#1683, thanks @Stashub). `detect` already classified a `#!/usr/bin/env bash`/`python`/`node`/... file as code, but `_get_extractor` dispatched on the suffix alone and returned nothing, so the file was silently dropped from the graph and its doc-referenced symbols stayed dangling stubs. Extensionless files now resolve their extractor from the shebang interpreter (`_SHEBANG_DISPATCH`), mirroring detection. Only interpreters with a real extractor are mapped (python, bash-family, node, ruby, lua, php, julia); others (perl, fish, Rscript) stay skipped rather than mis-parsed by a wrong grammar. +- Feat: Ruby `include`/`extend`/`prepend ` now emits a `mixes_in` edge to the module (#1668, thanks @krishnateja7). Concerns/mixins are the composition mechanism in Rails, but they produced no edges, so the blast radius of editing a shared concern was invisible to `affected`. A constant-argument mixin inside a class or module body now resolves to the module node (reusing the #1634 candidate logic and the #1640 module nodes, under the single-owner guard) and emits `Class --mixes_in--> Module`, which `affected` already traverses. `extend self` and non-constant arguments are skipped; an ambiguous or undefined module produces no edge. +- Feat: `affected ` now reaches callers that bind to the class's method nodes (#1669, thanks @krishnateja7). Since #1634 binds `Service.call` precisely to the `def self.call` method node, a class-level `affected` query missed those callers because `method`/`contains` are (correctly) not general-traversal relations. The reverse walk now seeds from the root's own member nodes (one `method`/`contains` hop outward) so method-bound callers are reachable from the class, with no change to the general traversal (no forward noise) and the member nodes themselves are not reported as hits. +- Fix: capitalized/mixed-case file extensions are no longer silently skipped (#1671, thanks @raman118). `collect_files` and `_get_extractor` matched suffixes case-sensitively, so `App.PY`, `script.JS`, `Lib.Ts`, etc. fell through and were never extracted. Suffix matching now falls back to the lowercased form for both file discovery and extractor dispatch (including `.blade.php`); an unsupported extension like `.xyz` is still skipped. +- Fix: the virtual PostgreSQL `source_file` URI no longer gets backslash-mangled on Windows (#1672, thanks @raman118). `introspect_postgres` built the synthetic `postgresql://host/db` path with `Path`, which rewrites `/` to `\` on Windows; it now uses `PurePosixPath` so the URI stays forward-slashed on every platform. +- Fix: a deferred `import(...)` no longer manufactures a phantom file cycle (#1241, thanks @Synvoya). Dynamic imports are real dependencies but not static ones, so two files that reference each other via one static import plus one dynamic import were reported as a circular dependency. The dynamic-import edge stays in the graph (marked `deferred`) but is excluded from `find_import_cycles`. +- Fix: an extractable source file that produces zero nodes is no longer cached, and is surfaced with a warning (#1666, thanks @krishnateja7). Every supported file yields at least a file node, so a zero-node result is anomalous (a transient batch/parallel hiccup). Caching it made the empty byte-stable across runs and silently blinded `affected`/`explain` to and through the file. The cache write is now skipped for a zero-node result so a rerun self-heals, and `extract` warns when an accepted source file lands in the graph with no nodes. This addresses the persistence and the silent blindness; if the underlying zero-node extraction still reproduces on a specific corpus, the warning now makes it visible to report. +- Fix: the Windows skill variant now declares `name: graphify` instead of `name: graphify-windows` (#1635, thanks @ray8875). `graphify install --platform windows` writes the variant to `~/.claude/skills/graphify/SKILL.md`, but Claude Code requires the skill folder name to equal the frontmatter `name`, so the `-windows` suffix broke discovery/validation. The variant suffix is a packaging detail, not part of the skill's identity. +- Fix: the OpenCode plugin joins its reminder to the user's command with `;` instead of `&&` (#1646, thanks @gonaik). Windows PowerShell 5.1 rejects `&&` as a statement separator (`not a valid statement separator`), so the first bash command of every OpenCode session on Windows failed. `;` works in PowerShell 5.1, Bash, and POSIX shells. (Both the OpenCode and Kilo plugin templates are fixed.) +- Fix: the `GRAPH_REPORT.md` "Import Cycles" section is now emitted only when the graph contains code (#1657, thanks @Ns2384-star). On a documents-only corpus there are no imports, so the section was pure noise ("None detected") on every run; it is now conditioned on code nodes or import edges being present. (The same report also confirms the mojibake and stdout-encoding items in that issue are already addressed on the current branch: manifest.json and `GRAPH_REPORT.md` are written UTF-8, and the CLI reconfigures stdout/stderr to UTF-8 with `errors="replace"`.) +- Fix: a modified `.docx`/`.xlsx` now re-enters `--update` (#1649, thanks @Ns2384-star). `detect_incremental` tracks the converted markdown sidecar, and `convert_office_file` early-returned whenever the sidecar already existed — so an Office source edited after its first conversion never updated its sidecar and was reported "unchanged" forever, freezing the graph on a living docs corpus. The sidecar is now re-converted when the source is newer than it (which bumps the sidecar's mtime/content so the incremental hash check picks it up); an unchanged source still skips the rewrite so it never churns (#1226). +- Fix: files whose absolute path exceeds Windows' 260-char limit are now hashed (#1655, thanks @Ns2384-star). `_md5_file`/`save_manifest`/`count_words` used plain `open()`/`stat()`, which the Windows file APIs reject for long paths unless prefixed with the extended-length marker `\\?\` — so deeply-nested files (accented, deep folders) never hashed, their manifest entry never stabilized, and `detect_incremental` re-flagged them as changed on every run. Change-detection I/O now prefixes long absolute paths on win32 (mirroring the normalization `cache.py` already applied to cache keys). No-op on other platforms. +- Perf: word counts are cached against each file's stat signature (#1656, thanks @Ns2384-star). `detect()` counted words in every PDF/docx/text file to size the corpus, re-opening and re-parsing every binary on each run — minutes on a large docs corpus even when only a few files changed. Counts are now memoized in the existing content-hash stat index (keyed by size + mtime), so an unchanged file is parsed once and read from the index thereafter; incremental detection drops from O(corpus) parsing to O(changed). +- Fix: a JS/TS call with no local definition and no import no longer binds to a same-named export in an unrelated package (#1659, thanks @leonaburime-ucla). When a callee had exactly one same-named definition repo-wide, the cross-file resolver emitted a `calls` edge at INFERRED/0.8 even with no import path between the two files. On a monorepo this fabricated dependencies: a 14-package repo showed `platform` and `sidecar` depending on `registry-protocol` purely because it exported generically-named symbols (`*Schema`, etc.) that unresolved calls collapsed onto. JS/TS modules have no implicit cross-module scope, so a cross-file call is real only if the caller imported it — direct JS/TS cross-file `calls` attribution is now gated on import evidence and left unresolved otherwise. Other languages keep the single-candidate resolution (C/C++ headers, Ruby autoload, same-package implicit scope legitimately call across files without an explicit import), and the `indirect_call` path (already INFERRED and callable-gated) is unchanged. As part of the fix, caller→file mapping for import-evidence now uses the raw call's `source_file` string, so a path-resolution/symlink mismatch can no longer spuriously fail evidence and mislabel a real cross-file call. + +## 0.9.6 (2026-07-04) + +- Fix: Ruby plain modules and `Struct.new` / `Class.new` / `Data.define` constant assignments now get container nodes (#1640, thanks @krishnateja7). The extractor only created nodes for `class Foo`, so `module Foo` (utility/`module_function` modules), `Foo = Struct.new(...) do ... end`, `Foo = Class.new(StandardError)`, and `Result = Data.define(...)` produced no node at all — their methods hung off the file via `contains` with dot-less labels, and no edge could ever target them. `module` is now a container type (methods attach via `method` like a class, nested modules included), and a constant assignment whose RHS is one of those factories synthesizes a class node named after the constant, attaches block-defined methods to it, and emits an `inherits` edge for `Class.new(Super)`. Plain constant assignments (`MAX = 100`, `X = Foo.new`) are untouched. +- Fix: Ruby constant-receiver singleton calls now resolve cross-file (#1634, thanks @krishnateja7). `Service.call`, `Model.where`, `SomeJob.perform_async` — the dominant Rails idiom — emitted no `calls` edge, so with Zeitwerk autoloading (no `require`s) a Rails app had essentially no cross-file edges and `affected`/`path` came up empty. `resolve_ruby_member_calls` now handles a capitalized (constant) receiver with any callee: it binds to the class's singleton/instance method when one is owned (`def self.call`, which the extractor indexes), else to the class node itself so inherited/dynamic class methods (ActiveRecord `where`/`find_by`) still give correct blast-radius. Namespaced receivers (`Billing::Processor.call`) resolve by the bare class name. The single-owning-class god-node guard is kept throughout — an ambiguous receiver resolves to nothing rather than a wrong edge. +- Fix: Apex `interface X extends A, B` now emits an `extends` edge per parent (#1645, thanks @Synvoya). The interface regex captured the parent list in group 2, but the handler only read the interface name (group 1), so multiple-inheritance parents were dropped and only the `contains` edge survived. The interface branch now iterates the parent list and resolves each the same way the class branch already does. +- Fix: Kotlin interface delegation (`class Foo : Bar by baz`) now emits the `implements` edge (#1644, thanks @Synvoya). The `by` form wraps the delegated interface in an `explicit_delegation` node, so neither the `constructor_invocation` nor the bare `user_type` branch fired and the edge was silently dropped. The delegation-specifier loop now unwraps `explicit_delegation` to its `user_type` (generic-argument recovery still runs), so idiomatic Kotlin delegation shows up in the graph. +- Fix: a malformed semantic chunk no longer crashes `extract` and discards every successful chunk (#1631, thanks @ssazy). When an LLM returned a well-formed object whose `edges` (or `nodes`/`hyperedges`) array carried a stray non-dict entry — a nested list where an edge object belongs — the AST+semantic merge and the semantic-cache write both called `.get()` per entry and raised `AttributeError: 'list' object has no attribute 'get'`. On a 34-chunk run where 33 succeeded, that meant no `graph.json` was written and the cache write failed too, so a re-run re-extracted everything. `_parse_llm_json` now sanitizes each fragment at the single parse chokepoint (keeping only dict entries and coercing a non-list value to `[]`), so the cache writer, the adaptive-retry merge, and the CLI merge are all protected in one place. +- Fix: an unresolved bare npm import no longer aliases onto an unrelated same-named local file (#1638, thanks @EveX1). `import colors from "tailwindcss/colors"` in a `.tsx` file emitted an `imports_from` edge to the bare id `colors`, and build.py's pre-migration alias index (which registers every local file's bare stem) then remapped it onto an unrelated `backend/utils/colors.py` — a confident (`EXTRACTED`) cross-language phantom edge, and one per `.tsx` file sharing the import. In a real monorepo eight unrelated `.tsx` files all landed on a single Python module. Common package subpaths (`colors`, `utils`, `types`, `config`, `client`) collide this way constantly. The external-import fallback now namespaces its target with the `ref` prefix (the same J-4 convention used for tsconfig `extends`/`$ref` externals), so it can never collapse to a local file/symbol id; the ref-namespaced target has no node, so build drops it as an external reference — the correct outcome for a third-party import. +- Fix: `graph.json` node/edge ordering is now stable run-to-run for document/semantic corpora (#1632, thanks @umeshpsatwe). With a parallel LLM backend, `extract_corpus_parallel` merged chunk results in completion order, so which network call happened to return first reordered the nodes and edges even when the model returned identical content — churning `graph.json` between otherwise-identical runs. Chunks are now merged in deterministic submission order after the pool drains (matching the serial path); the progress callback still fires in completion order so long local runs aren't silent. Note: the semantic content the LLM extracts is itself nondeterministic run-to-run — this fix removes the pipeline's own ordering churn, not the model's variance. + +- Fix: `graphify export obsidian` no longer crashes in `to_canvas` on a dangling community member (#1236 follow-up, thanks @swells808). The original #1236 fix guarded `to_obsidian` but not `to_canvas`, so a community member id with no backing node in the graph still raised `KeyError` while writing `graph.canvas` — after the notes had exported, leaving a partial mirror. `to_canvas` now applies the same dangling-member filter (`m in G and m in node_filenames`) in both the box-sizing and card-layout loops. + +- Feat: TS/JS member calls on a local `new` binding or a type-annotated parameter now resolve (#1630, thanks @DanielC000). `const s = new Svc(); s.doThing()` and a call on a typed param — including inside a returned closure (`(svc: Svc) => () => svc.doThing()`) — now emit `calls` edges to the receiver type's method, so `affected` no longer silently under-reports. Extends the #1316 `this.field` resolver: the per-file type table now also learns local `new` bindings and bare-typed parameters, and `walk_calls` descends into inline/returned closures (attributing their calls to the enclosing function) instead of stopping at the arrow boundary. Resolution keeps the single-definition guard; an untyped or non-bare-typed (array/union/generic) receiver produces no edge. + +- Fix: the `query` reference doc's inline vocab/fallback snippets now read and write files with `encoding="utf-8"` (#1619 A2, thanks @edtrackai). On Windows (default cp1252) the bare `read_text()`/`write_text()` calls crashed on exactly the cross-language corpora the doc demonstrates (e.g. Cyrillic labels like `обработчик`). Fixed across all generated skill variants. + +- Fix: `graphify update`/`watch` no longer leaves stale sources after a deletion or a destination-only rename (#1623 / #1622, thanks @oleksii-tumanov). When the last supported file was deleted, or a rename reported only its destination in `changed_paths`, the removed source's nodes lingered in `graph.json`. The rebuild now reconciles extractor-backed sources against the files still present (code and document sources, subdirectory roots, legacy markers, symlinks, hyperedges) while preserving semantic and out-of-scope records. +- Fix: `graphify query` guarantees per-term BFS seed diversity (#1596 / #1445, thanks @nokternol). A multi-term natural-language query could collapse to one seed when a single term hit an exact label match on an otherwise-unrelated node (`_EXACT_MATCH_BONUS` outscores substring matches ~1000×), and the 20%-gap seed cutoff then discarded every other term's seeds — so BFS explored only the incidental match's neighborhood. `_pick_seeds` now also seeds the best match for each distinct query term (ties broken by graph degree), so one term's incidental collision can't starve out the others. Partially addresses the seed-hijack in #1602. +- Fix: `extract` no longer crashes during final graph assembly when a node's `source_file` equals the scan root (#1618, thanks @sub4biz). Such a node (e.g. a project-level semantic concept the LLM attributed to the whole repo) relativized to `Path('.')`, and `_file_stem`'s `path.with_suffix("")` raised `ValueError: '.' has an empty name` — crashing *after* all LLM extraction cost was spent and writing no `graph.json` at all. `_file_stem` now returns `""` for a name-less path, and `_semantic_id_remap` skips the root-equal node (it has no per-file identity to remap, so its id is left untouched). Not a 0.9.5 regression — the latent code was hit only when dedup happened to produce a root-`source_file` node. +- Feat: C# receiver-typed member-call resolution (#1609, thanks @JensD-git). `recv.Method()` where `recv` is a typed field, property, parameter, or local now resolves to the receiver *type's* method. C# previously had no member-call resolver, so the bare method name matched any same-named method in the corpus — `_server.Save()` silently mis-bound to an unrelated `Cache.Save()` (a wrong edge, not just a missing one), leaving delegation-heavy call graphs blind across typed boundaries. The receiver is now typed from a per-file field/property/param/local table (incl. `var v = new T()`) and resolved with the single-definition god-node guard; `this.M()` binds to the enclosing class and `Type.M()` to the named type. An untypable receiver (e.g. `dynamic`) or a method absent on the type produces no edge — precision over recall, matching the Swift/C++/Python resolvers. +- Fix: `graphify cluster-only` now writes `.graphify_analysis.json` alongside `graph.json` (#1617 / #1610, thanks @sanmaxdev). Without it, a re-cluster left a stale/absent sidecar and a later `export html` silently reported "Single community". The sidecar now carries communities/cohesion/gods/surprises/questions, matching the full extract path. +- Fix: `.mts` / `.cts` (TypeScript module extensions) are now treated as TypeScript (#1607, thanks @ashmitg). They were missing from the code-extension set and the JS/TS language maps, so `.mts`/`.cts` sources were detected as non-code and silently skipped. +- Fix: four TS/JS extractor gaps (#1615, thanks @papinto). Generator functions (`function*`) now register as callables; `namespace`/`module`/`declare module` containers become queryable nodes; and the TS import-equals form (`import x = require("./m")`) now emits an import edge (its module string nests in an `import_require_clause` the direct-child scan missed). +- Fix: symlinked extraction inputs are contained to the scan root (#1613, thanks @Tok6Flow0). Symlink-directory following is now explicit opt-in, and resolved corpus paths must stay under the scan root before detection, AST collection, and LLM/image reads — an in-corpus symlink pointing outside the selected root is skipped rather than silently indexed. In-root symlinked sub-trees still work. +- Fix: the `claude-cli` backend no longer stalls on an infinite chunk bisection under newer Claude Code CLIs. The extraction schema was delivered via `--system-prompt` with only the raw file dump in the user turn, on the assumption that a replacement system prompt is the model's sole authority. Claude Code >= ~2.1 (verified on 2.1.197) does not honour that: it still layers in the local coding-agent context (CLAUDE.md/AGENTS.md in cwd, skills, MCP) and, given a user turn that is just a file with no request, replies conversationally ("I see the file, but there's no actual request attached — what would you like me to do with it?"). That prose parses to zero nodes/edges, so `_response_is_hollow` flagged it as truncation and the adaptive-retry path bisected the chunk indefinitely (`94 → 47 → 23 → …`), never converging and never writing `graph.json`. The full extraction schema plus an explicit imperative now ride in the user turn and `--system-prompt` is dropped, so the CLI emits the JSON object directly; the `` prompt-injection guardrails are carried verbatim and unchanged. Other `_call_claude_cli` behaviour (model override, `--add-dir` image handling, timeout, token accounting) is untouched. + +## 0.9.5 (2026-07-02) + +- Feat: the MCP server can serve many projects from one process via an optional `project_path` on every tool (#1594, thanks @joanfgarcia). Omit it and nothing changes — the server answers against the graph it was started with. Pass an absolute `project_path` and that call is routed to `//graph.json` instead, with its own mtime+size hot-reload, so one stdio/HTTP server backs a whole workspace of repos. Graphs load lazily and cache per resolved path; a missing/corrupt project graph is a tool error, not a process exit, and the server starts even when its default graph is absent. Backward-compatible and additive. +- Fix: Swift singleton cached into a local var now resolves later calls (#1604, thanks @jerryliurui). `let x = NetworkManager.shared` followed by `x.fetchData()` on a subsequent line produced zero call edges — local `let`/`var` bindings inside method bodies weren't typed (only class-level properties and params were), and a static-member init (`Type.shared`, a navigation expression) wasn't recognized even where locals were typed. Method-body locals are now typed from both constructor (`Type()`) and static-member (`Type.shared`) initializers, so `x.method()` resolves to the receiver type via the existing single-definition guard. This singleton-into-local idiom is one of the most common Swift call patterns. +- Fix: the skill's Python-interpreter detection now accepts Homebrew `python@3.x` paths (#1586, thanks @SUDARSHANCHAUDHARI). The shebang allowlist rejected any path with a character outside `[a-zA-Z0-9/_.-]`, but Homebrew installs versioned Python under `python@3.13`, so a valid interpreter containing `@` was skipped and detection fell through to a bare `python3` that lacked graphify (every step then failed with `ModuleNotFoundError`). `@` is now allowed across all skill variants (matching the #473 hooks.py fix); injection characters are still rejected. +- Fix: `graphify merge-graphs` no longer crashes on inputs that disagree on graph type (#1606, thanks @AdrianRusan). Per-repo `graph.json` files don't always share the same `directed` / `multigraph` flags, and `compose` requires one uniform type, so a mixed set raised an unhandled `NetworkXError`. All inputs are now normalized to a plain undirected graph (which the cross-repo merged view already is) before composing. +- Fix: type-reference / inheritance edge gaps closed across seven languages (all thanks @Synvoya): + - Scala: `var` field declarations now emit type `references` like `val` (#1587). + - PowerShell: class base types after `:` now emit `inherits` (first) / `implements` (rest), matching the C# convention (#1588). + - Objective-C: protocol-to-protocol adoption (`@protocol Derived `) now emits an `implements` edge (#1589). + - PHP: promoted constructor properties (`__construct(private Repo $r)`) now emit type `references` (method + class field) (#1590). + - C#: auto-properties (`public Widget Main { get; set; }`) now emit type `references` like fields, including generic args (#1591). + - C++: base-class template arguments (`class Car : Base`) now emit `generic_arg` references, matching the Java behavior (#1592). + - Swift: enum associated-value types (`case started(Session)`) now emit `references` (#1593). +- Fix: cross-file name resolution now respects case in case-sensitive languages (#1581, thanks @sheik-hiiobd). Resolution matched identifiers case-insensitively for every language, so in Python/Rust/Go/Java/etc. `from pathlib import Path` resolved to an unrelated shell-script `export PATH=...` node — a single variable becoming the corpus's #1 god-node (266 false incoming edges on one real repo), inflating god-node rankings, `affected` blast-radius, and community assignment. Both the cross-file call resolver and the type-reference stub-rewire now match by exact case; only genuinely case-insensitive languages (PHP functions/classes, SQL, Nim) still fold. For case-sensitive languages this only ever removes false edges. +- Fix: Julia qualified / relative / scoped-selected imports now emit edges (#1580, thanks @Synvoya). Only bare `using Foo` was handled; `using Base.Threads` (scoped), `using ..Parent` (relative import_path), and the scoped package of `import Base.Threads: nthreads` were dropped. +- Fix: Rust tuple-struct field types now emit `references` edges (#1582, thanks @Synvoya). `struct Wrapper(Logger, Vec);` referenced nothing — positional fields nest under `ordered_field_declaration_list` with no `field_declaration` wrapper, the same shape as tuple enum variants (#1579); that path wasn't traversed for structs. +- Fix: SystemVerilog class properties with leading qualifiers now emit field `references` (#1583, thanks @Synvoya). The field regex only matched unqualified ` ;`, so `rand Config x;` / `protected Base b;` (qualifier + type + name) failed to match and their type references were dropped. +- Fix: Elixir multi-alias brace form now emits imports edges (#1577, thanks @Synvoya). `alias Foo.{Bar, Baz}` produced no imports (the handler only matched a bare single alias); it now expands to one edge per member module. Single `alias`/`import`/`require`/`use` unchanged. +- Fix: Fortran function invocations now emit `calls` edges (#1578, thanks @Synvoya). Only `call sub(...)` (subroutine) calls were captured; `y = f(x)` function calls (a `call_expression`) were dropped. Resolved against procedures defined in the file so array indexing (`arr(i)`, same `name(...)` syntax) can't fabricate a spurious call. +- Fix: Rust enum variant payload types now emit `references` edges (#1579, thanks @Synvoya). `Click(Logger)` / `Resize { size: Dim }` referenced nothing — `enum_item` had no type-reference handler (struct/trait did). Both tuple and struct variant field types now resolve. +- Fix: `graphify cluster-only` no longer reuses stale community labels after the graph changed. When a repo was re-scoped/re-clustered, the saved `.graphify_labels.json` was applied wholesale to the new community set — so a community id that now covered a different community wore the old (LLM) name, silently. cluster-only now writes a per-community membership signature beside the labels and, on reuse, keeps a saved label only for communities whose membership is unchanged; any community that changed (or, for pre-signature label files, when the community count no longer matches) is renamed by its deterministic hub, with a warning to run `graphify label` for fresh LLM names. +- Fix: cross-file `indirect_call` edges were dropped by `graphify extract` on the CLI (a 0.9.4 regression). The callable-target guard for cross-file indirect dispatch was keyed on node ids collected before the id-relativization/disambiguation passes; when the scan root relativizes ids (the CLI's default, `cache_root == project root`), those ids went stale and every cross-file indirect edge was silently dropped — only same-file ones survived. Callable-ness is now read from a node marker that rides through the remaps, so `submit(imported_fn)`, imported dispatch tables, assignment/getattr aliases across files resolve on the CLI as they already did via the `extract()` API. + +## 0.9.4 (2026-07-01) + +- Fix: Ruby class inheritance now emits an `inherits` edge (#1535, thanks @Synvoya). `class Dog < Animal` produced `contains`/method/call edges but no `inherits` edge — the inheritance handler had branches for Java/Kotlin/C#/Scala/C++/PHP/Swift/Python but none for Ruby, so the `superclass` field was never read. Handles both bare (`< Animal`) and qualified (`< M::Base`) superclasses. +- Fix: Groovy `extends`/`implements` now emit `inherits`/`implements` edges (#1534, thanks @Synvoya). tree-sitter-groovy exposes inheritance through the same grammar shape as tree-sitter-java, but the handler was gated to Java only, so every Groovy inheritance relationship was dropped. +- Fix: corrupt `graph.json` now raises a clear, actionable error instead of a raw traceback (#1537 / #1536, thanks @guyoron1). The three graph-loading paths — `build_merge` (`--update`), `load_graph` (`graphify prs`), and diagnostics (`graphify diagnose`) — wrap `json.loads` and raise a `RuntimeError` with recovery guidance on a truncated/invalid file (incomplete write, power loss, manual edit). +- Fix: cross-chunk node-ID collisions now warn instead of silently dropping a node (#1508 / #1504, thanks @nuthalapativarun). When two nodes share an ID but come from different source files (two same-named files in different directories), dedup keeps the first and now prints a warning naming both files and how to avoid the loss (`graphify extract` per subfolder + `merge-graphs`). +- Fix: git hooks on Windows/MSYS default to sequential rebuilds (#1554, thanks @matiasduartee). Hook-triggered rebuilds now export `GRAPHIFY_MAX_WORKERS=1` on Windows/MSYS (explicit user value still wins), avoiding fragile inherited pipe handles; and the Windows-path hooks guard is a no-op on native Windows, where such paths are legitimate. +- Docs: correct the `deduplicate_by_label` docstring — it is dormant, not auto-called by `build()` (#1514, thanks @TPAteeq). The active dedup path is `deduplicate_entities`; the note that `deduplicate_by_label` runs automatically was never true, and it must not be enabled for code nodes (it merges by label with no file_type guard, conflating same-named symbols across files). +- Feat: deterministic hub community labels, readable without an LLM (#1576, thanks @sheik-hiiobd). When no LLM backend is configured, community labels used to fall back to `Community 70`, making the report and its Suggested Questions unreadable. Each community is now named after its highest-degree member (the structural hub, ties broken by node id for run-to-run stability) — so a plain `graphify` run reads `auth` / `log_action` at zero token cost. A configured LLM naming pass still overrides these with richer names; `--no-label` still yields bare `Community N`. +- Feat: extend `indirect_call` to `getattr(obj, "name")` reflective dispatch (#1575, #1566 slice 3, thanks @sheik-hiiobd). A callable looked up by a string literal — `fn = getattr(obj, "handler")` — now emits an `indirect_call` edge (context `getattr`, INFERRED) so `affected` reaches it. Only a plain string literal resolves; a variable, f-string, or concatenation is dynamic and emits nothing. Unlike the identifier paths, a getattr string names an attribute, not a binding, so it is never shadowed by a param/local — `def via(handler): getattr(x, "handler")` still resolves to the module `handler`. Function and module scope; cross-file handled by the shared resolver. Python only for now. +- Fix: `graphify --update` no longer drops hyperedges from unchanged files (#1574, thanks @socar-tender). `build_merge` read only nodes and edges from the existing `graph.json`, never hyperedges — so every incremental update collapsed the graph's hyperedge set (the semantic domain-flow groupings) down to just the re-extracted files'. Existing hyperedges are now carried forward: re-extracted files' prior hyperedges are replaced by their new version (by `source_file`), deleted files' are pruned, and the rest are preserved with id-dedup — mirroring how `watch` already handled it. +- Fix: `graphify --update` no longer leaves ghost nodes for deleted files when `build_merge` is called without `root` (#1571, thanks @goodjira). Absolute `prune_sources` paths (from `detect_incremental`) never relativized to match the stored relative `source_file` keys, so deleted files' nodes survived the prune. `build_merge` now infers a fallback root when none is passed — the committed `graphify-out/.graphify_root` marker, else the output dir's parent — so pruning (and re-extract replacement) work regardless of the caller. The shipped `--update` runbooks already pass `root`; this hardens the library for any caller that doesn't. +- Feat: extend `indirect_call` to assignment and return references (#1569, #1566 slice 2, thanks @sheik-hiiobd). A function bound to a name (`cb = handler`), returned from a factory (`def make(): return handler`), or aliased at module level (`CALLBACK = handler`) now emits an `indirect_call` edge, so `affected` reaches it. Captures the value side only (a bare name or a bare unpack `a, b = f, g`); a collection literal on the RHS stays with the dispatch-table scan. Reuses the shared guard, so the inverted-shadow trap is handled by construction — a param/local named on the RHS still hits the shadow guard and emits nothing (no return of #1565's false edges). Function and module scope; Python only for now. +- Fix: the skill-version mismatch warning is now direction-aware (#1568, thanks @TPAteeq). It used to advise `Run 'graphify install' to update` on ANY version difference, but `install` writes the package's own bundled skill and re-stamps the version — so when the skill on disk was NEWER than the package (a stale `uv tool` CLI, or a contributor's dev checkout), following that advice silently DOWNGRADED the skill to make the warning go away. Now when the skill is newer, the warning recommends upgrading the package (`uv tool upgrade graphifyy` / `pip install -U graphifyy`) instead; the older-skill case still recommends `install`. Versions compare numerically (so `0.10` > `0.9`). +- Feat: extend `indirect_call` capture to JS/TS (#1566). The same model now applies to JavaScript and TypeScript: a callback passed by name (`arr.map(fn)`, `setTimeout(fn)`, Express-style `app.get("/", handler)`, event wiring `emitter.on("e", handler)`) and functions listed in object/array dispatch tables (`const ROUTES = { create: handler }`, `const HOOKS = [onStart, onStop]`). Arrow-const functions (`const cb = () => {}`) count as callable targets; object shorthand (`{ handler }`) is a reference; inline arrows/function expressions are direct definitions and are not captured; object KEYS and non-callable values are excluded. Same guards as Python: callable-target-only, not shadowed by a param/local/module reassignment, single-definition god-node guard cross-file. Cross-file resolution is import-aware — a `import { onEvent }` edge to the symbol no longer suppresses the `indirect_call` to it. Module-level call-argument registration (idiomatic in JS) is captured in addition to the function-scoped capture Python has. +- Feat: extend `indirect_call` to dispatch tables (#1566). A function listed as a VALUE in a dict/list/set/tuple literal — a route/handler registry like `ROUTES = {"create": create_user, "delete": delete_user}` or `HOOKS = [on_start, on_stop]` — now emits an `indirect_call` edge so `affected` reaches those handlers too. Works at module level (attributed to the file) and inside a function (attributed to the function), same-file and cross-file. Same guards as the call-argument case: callable-target-only, not shadowed by a param/local/module-level reassignment, dict KEYS excluded (only values are references). +- Feat: capture indirect dispatch as `indirect_call` edges so `graphify affected` (blast radius) catches callers that pass a function by name as a call argument — `executor.submit(fn)`, `Thread(target=fn)`, `map(fn, xs)`, callbacks (#1565, thanks @sheik-hiiobd). Kept as a distinct INFERRED relation separate from `calls` (strict call-graph queries stay precise) and added to the affected relation set. Hardened against false edges: the argument name must resolve to a callable definition and must NOT be shadowed by a parameter or local binding in the enclosing function — so the idiomatic `def via(pool, handler): pool.submit(handler)` (handler is the param) and a data variable sharing a function's name produce no edge. Now also resolves cross-file: a callback imported from another module (`from .handlers import on_event; pool.submit(on_event)`) routes through the same cross-file resolver as direct calls — single-definition god-node guard, callable-target-only, staying INFERRED — closing the gap where #1565 saw only same-file callbacks (the common real-world shape is cross-module). Python only for now. + +## 0.9.3 (2026-06-30) + +- Feat: cross-file member-call resolution for C++ and Objective-C (#1547, #1556). A class declared in a header and defined in its `.cpp`/`.m` no longer fragments into two nodes (a decl/def merge pass collapses the sibling header/impl pair, gated to same-directory same-name so unrelated classes never merge), and a member call now resolves across files by the receiver's inferred type: C++ `Foo f; f.bar()` / `Foo::bar()` / `this->bar()` and ObjC `Foo *f = [[Foo alloc] init]; [f doThing]` / `[self render]` link to the owning class's method. Resolution is by receiver type, never bare name, with the single-definition god-node guard — an uninferable or ambiguous receiver produces no edge (high precision over recall, grounded in how compiler-free indexers like ctags/Doxygen mis-resolve by name). Also routes C++ headers to the C++ extractor and ObjC `#import` bridging headers to the ObjC extractor. Reported by @c0dezer019 and @JabberYQ. (Residual cross-file `#include` edge resolution under symlinked roots and ObjC dynamic-dispatch receivers remain follow-ups.) +- Feat: namespace-aware C# cross-file type resolution (#1562, thanks @TheFedaikin). The namespace is folded into the C# node id (so same-named types in different namespaces stay distinct), `using` directives are honored with lexical per-block scope, and qualified references (`Namespace.Type`, `using` aliases) resolve — disambiguating a bare reference to the one in-scope namespace that provides it, and refusing (no edge) when ambiguous. Advances the #1318 shadow-node umbrella for C#. +- Fix: test mocks no longer erase the real cross-file call graph (#1553, thanks @Schweinehund). When a bare callee name had 2+ definitions without unique import evidence, the god-node guard dropped the edge entirely — so a single same-named test mock wiped the real call graph (a 76-stub Pester suite erased everything). The guard now applies tie-breakers — non-test preference (a shared, segment-aware path classifier) then path proximity — and resolves only when exactly one candidate survives, else still bails. A real def plus a test mock resolves to the real def; two genuine non-test defs still bail (no fan-out). +- Fix: hyperedge member lists keyed `members` or `node_ids` are now accepted, not silently dropped (#1561, thanks @askalot-io). Normalized to the canonical `nodes` at ingest (in build_from_json and semantic_cleanup), deduped, with a warning — mirroring the existing from/to edge-endpoint aliasing. +- Feat: work-memory overlay — `graphify reflect` now projects the verdicts it distills (preferred / tentative / contested, recency-weighted) into a `.graphify_learning.json` sidecar next to graph.json, and `graphify explain` / `query` / `GRAPH_REPORT.md` / the HTML viewer surface them where you look (a `Lesson:` hint, a colored node ring). Builds on the idea in #1441/#1542 (thanks @TPAteeq), implemented as a sidecar rather than stamping graph.json: structural truth stays separate (no `learning_*` in graph.json or GraphML exports, no rebuild churn). Each verdict carries the source questions that produced it (provenance) and a content fingerprint of the cited code, so a verdict on a file that has changed since is flagged "code changed — re-verify" instead of shown as still-authoritative. Dead-ends stay query-scoped (a report section, never a node attribute). Letting verdicts influence query traversal is deliberately deferred (it needs propensity correction + exploration to avoid a self-reinforcing feedback loop). +- Feat: type-aware `this.field.method()` resolution for TypeScript/JS (#1316, thanks @guyoron1). A member call through a constructor-injected dependency (`constructor(private db: Database)` then `this.db.query()`) now produces a `calls` edge to the field type's method, resolved by the field's declared type and gated by the single-definition god-node guard (an ambiguous or untyped field produces no edge — no global name-match fan-out). EXTRACTED confidence; constructor parameter-property injection scope. +- Feat: resolve TypeScript wildcard path aliases (#1544, thanks @oleksii-tumanov). A `compilerOptions.paths` pattern like `@app/*` or `@*/interfaces` now captures the matched segment and substitutes it into each target in order, honoring tsc's longest-prefix / exact-wins specificity, baseUrl, and the first-existing-target fallback. Extends the #1531 resolver. +- Feat: resolve JS namespace re-export bindings (#1552, thanks @oleksii-tumanov). `export * as ns from './mod'` now creates a real symbol node for `ns`, registers it as a named export (so a downstream `import { ns }` resolves to it), and emits a file-level `re_exports` edge — treated as a single opaque binding, so `ns.member` accesses don't fan out into false per-symbol edges. Includes cycle and deep-chain guards. +- Feat: Objective-C dot-syntax property accesses and `@selector()` call edges (#1475, #1543, thanks @guyoron1). `self.product.name` now emits an `accesses` edge and `@selector(method)` a `calls` edge, each resolved only to an unambiguous in-scope definition by exact method-id match (a sibling of the same class for dot-syntax; exactly one method by exact selector name for `@selector`) — so `self.name` can't mis-resolve to a `-surname` sibling and same-named methods across classes don't fan out. Completes the #1475 ObjC follow-ups. + ## 0.9.2 (2026-06-29) - Feat: type-aware Ruby member-call resolution (#1499, thanks @vamsipavanmahesh). `p.run` is now resolved by the inferred type of the receiver (`p = Processor.new` ⇒ `Processor#run`) instead of by globally-unique method name, so the edge survives name collisions (an unrelated `Worker#run` no longer makes it ambiguous) and never points at the wrong method. Introduces a small resolver-registry framework that the existing Swift (#1356) and Python (#1446) cross-file passes register into. Receiver types are inferred only from unambiguous local `var = ClassName.new` bindings; a call whose receiver type can't be proven resolves to nothing rather than to a guess — a deliberate precision-over-recall change for Ruby member calls. diff --git a/README.md b/README.md index cbe7ae022..fe4500e33 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,54 @@

- Graphify + Graphify

- 🇺🇸 English | 🇨🇳 简体中文 | 🇯🇵 日本語 | 🇰🇷 한국어 | 🇩🇪 Deutsch | 🇫🇷 Français | 🇪🇸 Español | 🇮🇳 हिन्दी | 🇧🇷 Português | 🇷🇺 Русский | 🇸🇦 العربية | 🇮🇷 فارسی | 🇮🇹 Italiano | 🇵🇱 Polski | 🇳🇱 Nederlands | 🇹🇷 Türkçe | 🇺🇦 Українська | 🇻🇳 Tiếng Việt | 🇮🇩 Bahasa Indonesia | 🇸🇪 Svenska | 🇬🇷 Ελληνικά | 🇷🇴 Română | 🇨🇿 Čeština | 🇫🇮 Suomi | 🇩🇰 Dansk | 🇳🇴 Norsk | 🇭🇺 Magyar | 🇹🇭 ภาษาไทย | 🇺🇿 Oʻzbekcha | 🇹🇼 繁體中文 | 🇵🇭 Filipino + Graphify-Labs%2Fgraphify | Trendshift

+
+
Read this in other languages + +🇺🇸 English | 🇨🇳 简体中文 | 🇯🇵 日本語 | 🇰🇷 한국어 | 🇩🇪 Deutsch | 🇫🇷 Français | 🇪🇸 Español | 🇮🇳 हिन्दी | 🇧🇷 Português | 🇷🇺 Русский | 🇸🇦 العربية | 🇮🇷 فارسی | 🇮🇹 Italiano | 🇵🇱 Polski | 🇳🇱 Nederlands | 🇹🇷 Türkçe | 🇺🇦 Українська | 🇻🇳 Tiếng Việt | 🇮🇩 Bahasa Indonesia | 🇸🇪 Svenska | 🇬🇷 Ελληνικά | 🇷🇴 Română | 🇨🇿 Čeština | 🇫🇮 Suomi | 🇩🇰 Dansk | 🇳🇴 Norsk | 🇭🇺 Magyar | 🇹🇭 ภาษาไทย | 🇺🇿 Oʻzbekcha | 🇹🇼 繁體中文 | 🇵🇭 Filipino | 🇮🇱 עברית + +
+
+

- YC S26 - Discord - The Memory Layer - CI PyPI Downloads - Sponsor - LinkedIn - X + Discord + LinkedIn + YC S26

+Type `/graphify` in your AI coding assistant and it maps your entire project (code, docs, PDFs, images, videos) into a **knowledge graph** you can **query instead of grepping** through files. + +- **Code maps for free, fully local.** Code is parsed with tree-sitter AST: deterministic, no LLM, nothing leaves your machine. (Docs, PDFs, images and video use your assistant's model, or a configured API key, for a semantic pass.) +- **Every edge is explained.** Each connection is tagged `EXTRACTED` (explicit in the source) or `INFERRED` (resolved by graphify), so you can tell what was read directly from what was inferred. +- **Not a vector index.** No embeddings, no vector store: a real graph you traverse. Ask a question, trace the path between two things, or explain one concept. +

- - Star History Chart - + graphify's interactive graph.html showing the FastAPI codebase as a force-directed knowledge graph with a legend of detected communities

+

+ The FastAPI codebase mapped by graphify. Every node is a concept, colors are detected communities, and the whole thing is clickable in graph.html. +

+ +**Get started** (30 seconds): -Type `/graphify` in your AI coding assistant and it maps your entire project — code, docs, PDFs, images, videos — into a knowledge graph you can query instead of grepping through files. +```bash +uv tool install graphifyy # install the CLI (or: pipx install graphifyy) +graphify install # register the skill with your AI assistant +``` -Works in Claude Code, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, Amp, OpenClaw, Factory Droid, Trae, Hermes, Kimi Code, Kiro, Pi, Devin CLI, and Google Antigravity. +Then, in your AI assistant: ``` /graphify . ``` -That's it. You get three files: +That's it. You get **three files**: ``` graphify-out/ @@ -41,12 +57,68 @@ graphify-out/ └── graph.json the full graph — query it anytime without re-reading your files ``` -For a readable architecture page with Mermaid call-flow diagrams, run: +**Works in** Claude Code, Cursor, Codex, Gemini CLI, GitHub Copilot, and 15+ more — [pick your platform](#install). -```bash -graphify export callflow-html +--- + +## See it in action + +

+ graphify path query: a terminal asks for the shortest path between FastAPI and ModelField, and the answer lights up hop by hop across the knowledge graph +

+ +Once the graph is built you query it instead of reading files. Real output, graphify run on the FastAPI codebase shown above: + +```text +$ graphify explain "APIRouter" +Node: APIRouter + Source: routing.py L2210 + Community: 2 + Degree: 47 + +Connections (47): + --> RequestValidationError [uses] [INFERRED] + --> Dependant [uses] [INFERRED] + --> .get() [method] [EXTRACTED] + <-- __init__.py [imports] [EXTRACTED] + ... + +$ graphify path "FastAPI" "ModelField" +Shortest path (3 hops): + FastAPI --uses--> DefaultPlaceholder <--references-- get_request_handler() --references--> ModelField ``` +Every edge carries a **confidence tag** (`EXTRACTED` = explicit in the source, `INFERRED` = derived by resolution), so you can tell what was read directly from what was inferred. `graphify query ""` returns a scoped subgraph for a plain-language question, and `graphify path A B` traces how any two things connect. + +--- + +## What it does + +What you get out of the box: + +| Capability | What you get | +|---|---| +| **God nodes** | The most-connected concepts, so you see what everything flows through | +| **Communities** | The graph split into subsystems (Leiden), with LLM-free labels | +| **Cross-file links** | `calls` / `imports` / `inherits` / `mixes_in` resolved across ~40 languages via tree-sitter AST | +| **Query, path, explain** | Ask a question, trace the path between two things, or explain one concept, all against `graph.json` | +| **Rationale + doc refs** | `# NOTE:` / `# WHY:` comments and ADR/RFC citations become first-class nodes linked to the code | +| **Beyond code** | Docs, PDFs, images, and video/audio all map into the same graph | +| **Local-first** | Code is parsed locally with tree-sitter (no LLM, nothing leaves your machine); only the semantic pass over docs/media calls a backend, and only if you configure one | + +--- + +## Benchmarks + +| Benchmark | Metric | graphify | Field | +|---|---|---|---| +| LOCOMO (n=300) | recall@10 | **0.497** | mem0 0.048, supermemory 0.149 | +| LOCOMO (n=300) | QA accuracy | 45.3% | supermemory 49.7%, mem0 27.3% | +| LongMemEval-S (n=50) | QA accuracy | **76%** | tied with dense RAG | +| Graph build | LLM credits | **0** | per-token for most systems | + +Every system ran on the same harness with the same model and budgets, scored by a judge blind-validated against a second judge (90.6% agreement, Cohen's kappa 0.81). Full per-system tables, the code-intelligence result, and reproduction commands: **[BENCHMARKS.md](./BENCHMARKS.md)**. + --- ## Prerequisites @@ -124,7 +196,8 @@ for example `graphify claude install --project` or `graphify codex install --pro > **Git hooks and uv tool / pipx:** `graphify hook install` embeds the current interpreter path directly into the hook scripts at install time, so the post-commit hook fires correctly even in GUI git clients and CI runners where `~/.local/bin` is not on PATH. If you reinstall or upgrade graphify, re-run `graphify hook install` to refresh the embedded path. -### Pick your platform +
+Pick your platform (20+ assistants, click to expand) | Platform | Install command | |----------|----------------| @@ -152,15 +225,16 @@ for example `graphify claude install --project` or `graphify codex install --pro | Devin CLI | `graphify devin install` | | Google Antigravity | `graphify antigravity install` | -Codex users also need `multi_agent = true` under `[features]` in `~/.codex/config.toml` for parallel extraction. CodeBuddy uses the same Agent tool and PreToolUse hook mechanism as Claude Code. Factory Droid uses the `Task` tool for parallel subagent dispatch. OpenClaw and Aider use sequential extraction (parallel agent support is still early on those platforms). Trae uses the Agent tool for parallel subagent dispatch and does **not** support PreToolUse hooks — AGENTS.md is the always-on mechanism. +Codex users also need `multi_agent = true` under `[features]` in `~/.codex/config.toml` for parallel extraction. CodeBuddy uses the same Agent tool and PreToolUse hook mechanism as Claude Code. Factory Droid uses the `Task` tool for parallel subagent dispatch. OpenClaw and Aider use sequential extraction (parallel agent support is still early on those platforms). Trae uses the Agent tool for parallel subagent dispatch and does **not** support `PreToolUse` hooks, so AGENTS.md is the always-on mechanism. `--platform agents` (alias `--platform skills`) targets the generic cross-framework [Agent-Skills](https://github.com/anthropics/skills) locations: the spec's user-global `~/.agents/skills/` (read by `npx skills` and spec-compliant frameworks) for a global install, and `./.agents/skills/` for a project (`--project`) install. The bare `graphify install` stays single-platform (Claude Code) by design — use the named `agents` platform when you want the skill discoverable by any framework that reads `.agents/skills`. > Codex uses `$graphify` instead of `/graphify`. -### Optional extras +
-Install only what you need: +
+Optional extras (install only what you need) | Extra | What it adds | Install | |---|---|---| @@ -175,8 +249,6 @@ Install only what you need: | `leiden` | Leiden community detection (Python < 3.13 only) | `uv tool install "graphifyy[leiden]"` | | `ollama` | Ollama local inference | `uv tool install "graphifyy[ollama]"` | | `openai` | OpenAI / OpenAI-compatible APIs | `uv tool install "graphifyy[openai]"` | -| `minimax` | MiniMax OpenAI-compatible API (`--backend minimax`) | `uv tool install "graphifyy[minimax]"` | -| `nim` | NVIDIA NIM / AI Catalog OpenAI-compatible API (`--backend nim`) | `uv tool install "graphifyy[nim]"` | | `gemini` | Google Gemini API | `uv tool install "graphifyy[gemini]"` | | `anthropic` | Anthropic Claude API (`--backend claude`, uses `ANTHROPIC_API_KEY`) | `uv tool install "graphifyy[anthropic]"` | | `bedrock` | AWS Bedrock (uses IAM, no API key) | `uv tool install "graphifyy[bedrock]"` | @@ -185,9 +257,12 @@ Install only what you need: | `postgres` | Live PostgreSQL introspection (`--postgres DSN`) | `uv tool install "graphifyy[postgres]"` | | `dm` | BYOND DreamMaker `.dm`/`.dme` AST extraction (may need a C compiler + `python3-dev` if no wheel matches your platform) | `uv tool install "graphifyy[dm]"` | | `terraform` | Terraform / HCL `.tf`/`.tfvars`/`.hcl` AST extraction | `uv tool install "graphifyy[terraform]"` | +| `pascal` | Pascal / Delphi `.pas`/`.dpr`/`.dpk`/`.inc` AST extraction (more accurate `calls`/`inherits` edges; falls back to a regex extractor when absent) | `uv tool install "graphifyy[pascal]"` | | `chinese` | Chinese query segmentation (jieba) | `uv tool install "graphifyy[chinese]"` | | `all` | Everything above | `uv tool install "graphifyy[all]"` | +
+ --- ## Make your assistant always use the graph @@ -219,19 +294,24 @@ Run this once in your project after building a graph: | Devin CLI | `graphify devin install` | | Google Antigravity | `graphify antigravity install` | -This writes a small config file that tells your assistant to consult the knowledge graph for codebase questions — preferring scoped queries like `graphify query ""` over reading the full report or grepping raw files. On platforms that support payload-bearing hooks (Claude Code, Gemini CLI), a hook fires automatically before search-style tool calls (and, on Claude Code, before reading source files one by one via the Read/Glob tools) and nudges your assistant toward the graph path. On the others (Codex, OpenCode, Cursor, etc.), the persistent instruction files (`AGENTS.md`, `.cursor/rules/`, etc.) provide the same query-first guidance. `GRAPH_REPORT.md` is still available for broad architecture review. +This writes a small config file that tells your assistant to consult the knowledge graph for codebase questions, preferring scoped queries like `graphify query ""` over reading the full report or grepping raw files. -**CodeBuddy** does the same two things as Claude Code: writes a `CODEBUDDY.md` section telling CodeBuddy to read `graphify-out/GRAPH_REPORT.md` before answering architecture questions, and installs **PreToolUse hooks** (`.codebuddy/settings.json`) that fire before Bash search commands and file reads, nudging toward `graphify query` instead. +- **Hook platforms** (Claude Code, Gemini CLI): a hook fires automatically before search-style tool calls (and, on Claude Code, before reading source files one by one via the Read/Glob tools) and nudges your assistant toward the graph path. +- **Instruction-file platforms** (Codex, OpenCode, Cursor, etc.): persistent instruction files (`AGENTS.md`, `.cursor/rules/`, etc.) provide the same query-first guidance. -**Codex** writes to `AGENTS.md` and also installs a **PreToolUse hook** in `.codex/hooks.json` that fires before every Bash tool call — same always-on mechanism as Claude Code. +`GRAPH_REPORT.md` is still available for broad architecture review. -To remove graphify from all platforms at once: `graphify uninstall` (add `--purge` to also delete `graphify-out/`). Or use the per-platform command (e.g. `graphify claude uninstall`). +**CodeBuddy** does the same two things as Claude Code: writes a `CODEBUDDY.md` section telling CodeBuddy to read `graphify-out/GRAPH_REPORT.md` before answering architecture questions, and installs `PreToolUse` hooks (`.codebuddy/settings.json`) that fire before Bash search commands and file reads, nudging toward `graphify query` instead. ---- +**Codex** writes to `AGENTS.md` and also installs a `PreToolUse` hook in `.codex/hooks.json` that fires before every Bash tool call, same always-on mechanism as Claude Code. -**Kilo Code** installs the Graphify skill to `~/.config/kilo/skills/graphify/SKILL.md` and a native `/graphify` command to `~/.config/kilo/command/graphify.md`. `graphify kilo install` also writes `AGENTS.md` plus a native **`tool.execute.before` plugin** (`.kilo/plugins/graphify.js` + `.kilo/kilo.json` or `.kilo/kilo.jsonc` registration) so Kilo gets the same always-on graph reminder behavior through native `.kilo` config. +**Kilo Code** installs the Graphify skill to `~/.config/kilo/skills/graphify/SKILL.md` and a native `/graphify` command to `~/.config/kilo/command/graphify.md`. `graphify kilo install` also writes `AGENTS.md` plus a native `tool.execute.before` plugin (`.kilo/plugins/graphify.js` + `.kilo/kilo.json` or `.kilo/kilo.jsonc` registration) so Kilo gets the same always-on graph reminder behavior through native `.kilo` config. -**Cursor** writes `.cursor/rules/graphify.mdc` with `alwaysApply: true` — Cursor includes it in every conversation automatically, no hook needed. +**Cursor** writes `.cursor/rules/graphify.mdc` with `alwaysApply: true`, so Cursor includes it in every conversation automatically, no hook needed. + +To remove graphify from all platforms at once: `graphify uninstall` (add `--purge` to also delete `graphify-out/`). Or use the per-platform command (e.g. `graphify claude uninstall`). + +--- ## What's in the report @@ -247,7 +327,7 @@ To remove graphify from all platforms at once: `graphify uninstall` (add `--purg | Type | Extensions | |------|-----------| -| Code (36 tree-sitter grammars) | `.py .ts .js .jsx .tsx .mjs .go .rs .java .c .cpp .h .hpp .cu .cuh .metal .rb .cs .kt .scala .php .swift .lua .luau .zig .ps1 .psm1 .ex .exs .m .mm .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .xaml .razor .cshtml` (`.dm`/`.dme` requires `uv tool install graphifyy[dm]`; CUDA `.cu`/`.cuh` and Metal `.metal` reuse the C++ grammar) | +| Code (36 tree-sitter grammars) | `.py .ts .mts .cts .js .jsx .tsx .mjs .go .rs .java .c .cpp .cc .cxx .h .hpp .cu .cuh .metal .rb .cs .kt .kts .scala .php .swift .lua .luau .toc .zig .ps1 .psm1 .psd1 .ex .exs .m .mm .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .xaml .razor .cshtml` (`.dm`/`.dme` requires `uv tool install graphifyy[dm]`; `.mts`/`.cts` reuse the TypeScript grammar, `.cc`/`.cxx` and CUDA `.cu`/`.cuh` and Metal `.metal` reuse the C++ grammar) | | Salesforce Apex | `.cls .trigger` (regex-based; classes, interfaces, enums, methods, triggers, SOQL/DML edges) | | Terraform / HCL | `.tf .tfvars .hcl` (requires `uv tool install graphifyy[terraform]`) | | MCP configs | `.mcp.json` `mcp.json` `mcp_servers.json` `claude_desktop_config.json` — extracts server nodes, package refs, env var requirements | @@ -260,7 +340,7 @@ To remove graphify from all platforms at once: `graphify uninstall` (add `--purg | Video / Audio | `.mp4 .mov .mp3 .wav` and more (requires `uv tool install graphifyy[video]`) | | YouTube / URLs | any video URL (requires `uv tool install graphifyy[video]`) | -Code is extracted locally with no API calls (AST via tree-sitter). Everything else goes through your AI assistant's model API. +Code is extracted **locally with no API calls** (AST via tree-sitter). Everything else goes through your AI assistant's model API. Google Drive for desktop `.gdoc`, `.gsheet`, and `.gslides` files are shortcut pointers, not document content. To include native Google Docs, Sheets, and Slides @@ -314,7 +394,7 @@ See the [full command reference](#full-command-reference) below. Create a `.graphifyignore` in your project root — same syntax as `.gitignore`, including `!` negation. -**`.gitignore` is respected automatically.** Graphify loads `.gitignore` first, then `.graphifyignore`, so project-wide data/log/vendor exclusions apply and graphify-specific rules can override them with normal last-match-wins semantics. Subdirectory scoping works the same way as git — an ignore file only affects its own subtree. +**`.gitignore` is respected automatically.** graphify reads the `.gitignore` in each directory. If a `.graphifyignore` is also present, the two are **merged** — `.graphifyignore` patterns are evaluated last, so they win on conflicts (including `!` negations). Adding a `.graphifyignore` only ever excludes more; it never re-includes a file your `.gitignore` already excluded. Subdirectory scoping works the same way as git — an ignore file only affects its own subtree. ``` # .graphifyignore @@ -403,36 +483,25 @@ docker run -p 8080:8080 -v "$(pwd)/graphify-out:/data" graphify \ ## Environment variables -These are only needed for **headless / CI extraction** (`graphify extract`) or when you want the `/graphify` skill to use a direct backend instead of the host assistant's own model. Automatic semantic extraction starts with local Ollama for laptop-safe <=8B-class models, tries the local fallback chain (`qwen2.5-coder:3b` → `gemma3:4b` by default), and uses MiniMax as the final spillover when local chunks are slow, too large, or laptop load is high. NVIDIA NIM remains available only when explicitly selected. +These are only needed for **headless / CI extraction** (`graphify extract`). When running via the `/graphify` skill inside your IDE, the model API is provided by your IDE session — no extra keys needed. | Variable | Used for | When required | |---|---|---| -| `OLLAMA_BASE_URL` | Ollama local inference URL | optional — default `http://localhost:11434/v1` | -| `GRAPHIFY_OLLAMA_MODEL` or `OLLAMA_MODEL` | Ollama model name | optional — default `qwen2.5-coder:3b`; must include a size and stay within the <=8B local safety class | -| `GRAPHIFY_OLLAMA_FALLBACK_MODELS` | Ordered local Ollama fallback models | optional — default `qwen2.5-coder:3b,gemma3:4b`; set `none` to disable local model fallback | -| `GRAPHIFY_OLLAMA_TOKEN_BUDGET` | Ollama semantic chunk packing cap | optional — default `20000`; keeps prompt + output inside the 32k local context before adaptive retry | -| `GRAPHIFY_OLLAMA_NUM_CTX` | Override Ollama KV-cache window size | optional — auto-sized by default | -| `GRAPHIFY_OLLAMA_KEEP_ALIVE` | Time to keep Ollama model loaded | optional — default `30s`; set `0` to unload after each chunk | -| `GRAPHIFY_OLLAMA_NUM_GPU` | Ollama GPU layer offload target | optional — default `999` to keep the local model on GPU | -| `GRAPHIFY_OLLAMA_MAIN_GPU` | Ollama GPU index | optional — default `0` | -| `GRAPHIFY_OLLAMA_NUM_THREAD` | Ollama CPU helper thread cap | optional — default `min(4, CPU/4)` with floor `2`; keeps GPU-fed local runs responsive without stealing daily-driving CPU | -| `GRAPHIFY_OLLAMA_BALANCE` | Ollama/MiniMax balancing | optional — `auto` (default), `local`, `remote`, or `defer` | -| `GRAPHIFY_OLLAMA_MINIMAX_MAX_FRACTION` | Cost cap for dynamic MiniMax spillover | optional — default `0.25` | -| `GRAPHIFY_DISABLE_MINIMAX_FALLBACK` | Disable Ollama→MiniMax cloud fallback | optional — set `1` for strict local-only semantic extraction | -| `MINIMAX_API_KEY` or `GRAPHIFY_MINIMAX_API_KEY` | MiniMax OpenAI-compatible token-plan fallback | `--backend minimax` or dynamic spill/fallback when Ollama is slow or fails | -| `GRAPHIFY_MINIMAX_MODEL` or `MINIMAX_MODEL` | MiniMax model override | optional — default `MiniMax-M3` | -| `NVIDIA_NIM_API_KEY`, `GRAPHIFY_NVIDIA_NIM_API_KEY`, `NVIDIA_API_KEY`, or `NGC_API_KEY` | NVIDIA NIM / AI Catalog backend | explicit `--backend nim` only | -| `GRAPHIFY_NVIDIA_NIM_MODEL`, `NVIDIA_NIM_MODEL`, or `NIM_MODEL` | NVIDIA NIM model override | optional — default `meta/llama-3.1-8b-instruct` | -| `NVIDIA_NIM_BASE_URL` or `NIM_BASE_URL` | NVIDIA NIM endpoint override | optional — default `https://integrate.api.nvidia.com/v1` | | `ANTHROPIC_API_KEY` | Claude (Anthropic) backend | `--backend claude` | | `ANTHROPIC_BASE_URL` | Anthropic-compatible endpoint URL (LiteLLM proxy, gateways, ...) | `--backend claude` (default: `https://api.anthropic.com`) | | `ANTHROPIC_MODEL` | Model name for the Claude backend — for custom endpoints, use the model name/alias your server exposes | `--backend claude` (default: `claude-sonnet-4-6`) | | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | Google Gemini backend | `--backend gemini` | | `OPENAI_API_KEY` | OpenAI or OpenAI-compatible APIs | `--backend openai` (local servers accept any non-empty value) | | `OPENAI_BASE_URL` | OpenAI-compatible server URL (llama.cpp, vLLM, LM Studio, ...) | `--backend openai` (default: `https://api.openai.com/v1`) | -| `OPENAI_MODEL` | Model name for the OpenAI backend — for self-hosted servers, use the model name/alias your server exposes | `--backend openai` (default: `gpt-4.1-mini`) | +| `OPENAI_MODEL` | Model name for the OpenAI backend — for self-hosted servers, use the model name/alias your server exposes (check its `/v1/models` endpoint), e.g. `LFM2.5-8B-A1B-UD-Q4_K_XL` for llama.cpp | `--backend openai` (default: `gpt-4.1-mini`) | | `DEEPSEEK_API_KEY` | DeepSeek backend | `--backend deepseek` | | `MOONSHOT_API_KEY` | Kimi Code backend | `--backend kimi` | +| `MINIMAX_API_KEY` or `GRAPHIFY_MINIMAX_API_KEY` | MiniMax fallback backend | `--backend minimax` or automatic Ollama spill/fallback | +| `NVIDIA_NIM_API_KEY` or `NVIDIA_API_KEY` | NVIDIA NIM backend | `--backend nim` | +| `OLLAMA_BASE_URL` | Ollama local inference URL | auto-detected first, or `--backend ollama` (default: `http://localhost:11434/v1`) | +| `GRAPHIFY_OLLAMA_MODEL` or `OLLAMA_MODEL` | Ollama model name | `--backend ollama` (default: `qwen2.5-coder:3b`) | +| `GRAPHIFY_OLLAMA_NUM_CTX` | Override Ollama KV-cache window size | optional — auto-sized by default | +| `GRAPHIFY_OLLAMA_KEEP_ALIVE` | Minutes to keep Ollama model loaded | optional — set `0` to unload after each chunk | | `AZURE_OPENAI_API_KEY` | Azure OpenAI Service backend | `--backend azure` | | `AZURE_OPENAI_ENDPOINT` | Azure resource endpoint URL | `--backend azure` (required alongside API key) | | `AZURE_OPENAI_API_VERSION` | Azure API version override | optional — default `2024-12-01-preview` | @@ -446,16 +515,13 @@ These are only needed for **headless / CI extraction** (`graphify extract`) or w | `GRAPHIFY_GOOGLE_WORKSPACE` | Auto-enable Google Workspace export | optional — set to `1` | | `GRAPHIFY_TRIAGE_BACKEND` | Backend for `graphify prs --triage` | optional — auto-detected from available keys | | `GRAPHIFY_TRIAGE_MODEL` | Model override for triage | optional — e.g. `claude-opus-4-7` | -| `GRAPHIFY_QUERY_LOG` | Override query log path (default: `~/.cache/graphify-queries.log`) | optional — set to empty or `/dev/null` to silence | -| `GRAPHIFY_QUERY_LOG_DISABLE` | Set to `1` to disable query logging entirely | optional | -| `GRAPHIFY_QUERY_LOG_RESPONSES` | Set to `1` to also log full subgraph responses (off by default) | optional | +| `GRAPHIFY_QUERY_LOG_ENABLE` | Set to `1` to turn on the local query log at `~/.cache/graphify-queries.log` (records each query/path/explain question + corpus path). Off by default — nothing is written unless you opt in (#1797) | optional | +| `GRAPHIFY_QUERY_LOG` | Enable the query log and write it to this path instead of the default | optional — off unless this or `_ENABLE` is set | +| `GRAPHIFY_QUERY_LOG_DISABLE` | Set to `1` to force the query log off (wins over the enable vars) | optional | +| `GRAPHIFY_QUERY_LOG_RESPONSES` | When the log is enabled, also record full subgraph responses (off by default) | optional | | `GRAPHIFY_MAX_GRAPH_BYTES` | Override the 512 MiB graph.json size cap — e.g. `700MB`, `2GB`, or plain bytes | optional — useful for very large corpora | | `GRAPHIFY_LLM_TEMPERATURE` | Override LLM temperature for semantic extraction — e.g. `0.7`, or `none` to omit | optional — auto-omitted for o1/o3/o4/gpt-5 reasoning models | -For user-wide MiniMax defaults that work even when a coding agent is launched without your shell environment, put the key in `~/.graphify/credentials.json` as `{"api_keys":{"MINIMAX_API_KEY":"..."}}` and keep that file out of git. - -For semantic rebuilds that can wait, run daytime commands with `GRAPHIFY_OLLAMA_BALANCE=defer`; graphify writes `graphify-out/semantic-rebuild-queue.jsonl` with the night-window rebuild hint. Use `graphify update .` immediately for low-load AST indexing, then run queued semantic rebuilds after 20:00 when the laptop is idle (03:00-06:00 remains the safest window). - --- ## Privacy @@ -464,7 +530,7 @@ For semantic rebuilds that can wait, run daytime commands with `GRAPHIFY_OLLAMA_ - **Video / audio** — transcribed locally with faster-whisper. Nothing leaves your machine. - **Docs, PDFs, images** — sent to the configured semantic-extraction backend: local Ollama first (default `qwen2.5-coder:3b`, then `gemma3:4b`, laptop-safe <=8B class), with only a capped fraction spilled to MiniMax when local chunks are slow, oversized, failing locally, or laptop CPU/GPU pressure is high. - **Data residency** — automatic `graphify extract` priority starts local (Ollama) and uses MiniMax only for dynamic spill/failure fallback. Ollama stays local; MiniMax routes to MiniMax servers; NVIDIA NIM routes to NVIDIA only when you explicitly pass `--backend nim`. -- No telemetry, no usage tracking, no analytics. +- **No telemetry**, no usage tracking, no analytics. - **Query logging** — every `graphify query`, `graphify path`, `graphify explain`, and MCP `query_graph` call is logged to `~/.cache/graphify-queries.log` in JSON Lines format (timestamp, question, corpus, nodes returned, duration). Full subgraph responses are **not** stored by default. Set `GRAPHIFY_QUERY_LOG_DISABLE=1` to opt out, or `GRAPHIFY_QUERY_LOG=/dev/null` to silence without disabling the code path. --- @@ -532,6 +598,14 @@ uv tool upgrade graphifyy graphify install # overwrites the skill file ``` +**Claude Code prompt cache invalidated after every `graphify extract`** +Graphify writes output files (`graph.json`, `graphify-out/`) into the workspace. If those paths aren't ignored, every write invalidates Claude Code's prompt cache, forcing a full re-upload at cache-write rates on the next turn. Add them to `.claudeignore`: +```text +# .claudeignore +graph.json +graphify-out/ +``` + --- ## Full command reference @@ -569,7 +643,9 @@ graphify save-result --question "Q" --answer "A" --nodes Foo Bar --outcome usefu graphify reflect # aggregate graphify-out/memory/ outcomes into reflections/LESSONS.md graphify reflect --if-stale # no-op when LESSONS.md is already newer than every input (cheap to run each session) graphify reflect --out docs/LESSONS.md # write the lessons doc somewhere else -graphify reflect --graph graphify-out/graph.json # also group lessons by community +graphify reflect --graph graphify-out/graph.json # group lessons by community + write the work-memory overlay (.graphify_learning.json) + # the overlay tags nodes preferred/tentative/contested (recency-weighted, with provenance); + # graphify explain / query then show a "Lesson:" hint, flagged "code changed — re-verify" when the source moved on graphify uninstall # remove from all platforms in one shot graphify uninstall --purge # also delete graphify-out/ @@ -619,10 +695,10 @@ graphify devin uninstall graphify antigravity install # .agents/rules + .agents/workflows (Google Antigravity) graphify antigravity uninstall -graphify extract ./docs # headless LLM extraction; auto: laptop-safe Ollama primary, capped MiniMax spillover +graphify extract ./docs # headless LLM extraction for CI (no IDE needed) graphify extract ./docs --backend gemini # explicit backend: ollama, minimax, nim, gemini, kimi, claude, openai, deepseek, bedrock, or claude-cli graphify extract ./docs --backend gemini --model gemini-3.1-pro-preview -graphify extract ./docs --backend ollama # local Ollama (default qwen2.5-coder:3b) - no API key needed for loopback +graphify extract ./docs --backend ollama # local Ollama (set OLLAMA_BASE_URL / OLLAMA_MODEL) - no API key needed for loopback OPENAI_BASE_URL=http://localhost:8080/v1 OPENAI_MODEL=my-model graphify extract ./docs --backend openai # any OpenAI-compatible server (llama.cpp, vLLM, LM Studio) ANTHROPIC_BASE_URL=http://localhost:4000 ANTHROPIC_MODEL=my-model graphify extract ./docs --backend claude # any Anthropic-compatible endpoint (LiteLLM proxy, gateways) GRAPHIFY_OLLAMA_NUM_CTX=32768 graphify extract ./docs --backend ollama # override KV-cache window (auto-sized by default) @@ -668,7 +744,7 @@ graphify clone https://github.com/karpathy/nanoGPT graphify merge-graphs a.json b.json --out merged.json graphify --version # print installed version graphify watch ./src -graphify check-update ./src # prints pending semantic/night-window hints; never runs heavy work +graphify check-update ./src graphify update ./src graphify update ./src --no-cluster # skip reclustering, write raw AST graph only graphify update ./src --force # overwrite even if new graph has fewer nodes @@ -678,7 +754,8 @@ graphify cluster-only ./my-project --max-concurrency 16 --batch-size 200 # para graphify cluster-only ./my-project --resolution 1.5 # more, smaller communities graphify cluster-only ./my-project --exclude-hubs 99 # exclude p99 degree nodes from partitioning graphify cluster-only ./my-project --no-label # keep "Community N" placeholders -graphify cluster-only ./my-project --backend=ollama # backend for community naming +graphify cluster-only ./my-project --backend=gemini # backend for community naming +graphify cluster-only ./my-project --backend=gemini --model gemini-2.5-pro # specific model graphify label ./my-project # (re)name communities with the configured backend graphify label ./my-project --backend=openai --model gpt-4o # force a specific backend and model ``` @@ -692,16 +769,17 @@ graphify label ./my-project --backend=openai --model gpt-4o # force a specific - [How it works](docs/how-it-works.md) — the extraction pipeline, community detection, confidence scoring, benchmarks - [ARCHITECTURE.md](ARCHITECTURE.md) — module breakdown, how to add a language - [Optional integrations](docs/docker-mcp-sqlite.md) — Docker MCP Toolkit + SQLite +- [The Memory Layer](https://safishamsi.gumroad.com/l/qetvlo) — the book on the ideas behind graphify, the architecture end to end --- -## Built on graphify — Penpax +## Built on graphify: Penpax -[**Penpax**](https://graphifylabs.ai) is the always-on layer built on top of graphify — it applies the same graph approach to your entire working life: meetings, browser history, emails, files, and code, updating continuously in the background. +[**Penpax**](https://graphify.com) is the always-on layer built on top of graphify — it applies the same graph approach to your entire working life: meetings, browser history, emails, files, and code, updating continuously in the background. Built for people whose work lives across hundreds of conversations and documents they can never fully reconstruct. No cloud, fully on-device. -**Free trial launching soon.** [Join the waitlist →](https://graphifylabs.ai) +**Free trial launching soon.** [Join the waitlist →](https://www.graphify.com) --- @@ -755,3 +833,22 @@ uv run pytest tests/ -q -k "python" # filter by name See [ARCHITECTURE.md](ARCHITECTURE.md) for module responsibilities and how to add a language. + +--- + +

+ + Star History Chart + +

+ +--- + +## Community and links + +

+ Discord + X + Sponsor + The Memory Layer +

diff --git a/SECURITY.md b/SECURITY.md index 297b7d8d3..795454b21 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -42,7 +42,7 @@ graphify is a **local development tool**. It runs as a Claude Code skill and opt ### What graphify does NOT do -- Does not run a network listener (MCP server communicates over stdio only) +- Does not run a network listener by default (stdio transport); `--transport http` is opt-in, documented in the README, and binds to `127.0.0.1` unless `--host 0.0.0.0` is passed - Does not execute code from source files (tree-sitter parses ASTs - no eval/exec) - Does not use `shell=True` in any subprocess call - Does not store credentials or API keys diff --git a/docs/demo-path.svg b/docs/demo-path.svg new file mode 100644 index 000000000..4bf10a6e8 --- /dev/null +++ b/docs/demo-path.svg @@ -0,0 +1 @@ +graphify$graphifypath"FastAPI""ModelField"Shortest path (3 hops):FastAPI --uses--> DefaultPlaceholder <--references-- get_request_handler() --references--> ModelField3 hops. Zero files opened.usesreferencesreferencesFastAPIDefaultPlaceholderget_request_handler()ModelField \ No newline at end of file diff --git a/docs/graph-hero.png b/docs/graph-hero.png new file mode 100644 index 0000000000000000000000000000000000000000..f4968f6d120345f290a580e950fb35b72c7a10f2 GIT binary patch literal 648908 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

ev`bu9MF;jJTUqr-{eYFAw= z_i_OjB+zO>Uc`yZDkAwE1Vp*jBCr0lba1j6_%3wXeh6b|*!qOmo~l@UU!2=-2E*x- z?MW7%ZiZ5xKD(edB1(S!6590h?FxZ>CAqGG%Qp2rxs0Wptn)qtCr%E?6dN>juRuev zByVX?vv#;F6G^VB!;1%^k_gIeczu<-WA14=czdI~B=jd2_ElXc$p)cU>PH4fdB=2T8 zU0_p-dp}wryTAjF{gzx{!&FGX0l~>eAkLd*$h|4axrxjX$s##MA9_tlS{Bj}Pa8*% zc1BRji!oy>1)-O9q@CZH>fn`yQ$y$Z_;Ml6P%hb|aj{3s5_q$tQMpDrWU83&O>ONt z>)grF{*v4CESc`CiC&qYekK#rW)Oh-;rWZ~dSJA#Furxv360Y>=SoUV)16RDa`AUC z=!A1`wh)Uy?!?i@w!3f4oQ%ei;b&5zdgP;I!Ky2b`$Gy}jY&}}(jzk4!lFKWzmE1@ z`y-(5=*6D=;aF`0KhNjrMf6_1f`NVGAVF=XlKX|}U8v-$G_(Uj8vx~6#S*h)(f z7$U`y#~5`we;L|`MPUbX28$i&BvcQxHm(mRCwsV+_Xpq1hA^Tfklo79Uvl(Y>7ApU zR7Z=$g&!nj6uWK;(n2~!gf(16oN2_7*-+7oJ+t3e7fb|g<{p&)LjnL)K#333zUt!8 zkG;vZ_eqT}OgGL3u9i|pf0)x*_+LB>Lrv6yMmT^c6mllkQ&rz?K1=IraOE>sBR+^2 zV4F^#8cl{!7nPmI%5lEvnYj9uT1js_n~%3l(X2-Co#qjf`?xu|5EKkg@qIZ&mXp}!nwHyh8MYy_CKF~JKFA65xJyYKOSd-`7d@s^B z?*q~rkG(&9*Jfos8z z4$(6FB|GA(5f%+n#9sTmtoWw9Ooka)S6e)Ov}%icxz=xn(j)~BkFp-W<)hH2sQe=9 z86B>d9S-+9XHf|)rS@K1U6WA>B>2@OUu5o#XHAziFl!j}Ag~hNIoE~`l+I=?9j2X{ zG~*I3A--zk?E;vB^~<_MMaj7o8WFc- z%i1p2AelJo*f*@eYD+B!RhH z>ocH|ivvDBBEkR3VM(ELQopZQCR@yOtT_7CIudc~E8S6V;d8vz+<#s*HqO%2aUXs5 zAZ&q%!d37(DdicHW~+GY=N4^kFei%>z21LN(ogSbd_H$kYl~%_^Otqm(?sTD(dalI8|24!2|0*xVmeodmm1LI=3hU z1~Bjm3awLr2xZYXXYXwHS;yID4v5TmlC0f|I~r&cO?a9r)3zlS{IkqKAu)56dL&+ z4R^Rdj5PnM#+G?ow8MnwzLUZ7BUq7_ga%wOoya)GJfmY_(P~BvnAVmX%)V@5 z__w9;#LQ&OE{4VO>5kp>l~m8)er7%3tJK9kms2Gw!p6-&)=4kU7V!cc&1zg?6LV9m zb20IT@;B`#UgVpkR9jyDwO0J;?!!(QP~UM6;v!T4EZ1zj02&VUKY2n21w{eN(y3=Z zdn|U8WX}IOY&^k7Fb2$l{YA}!g6>BRN66=;08A{!b`T;6j>h~vXvYKU zhT1MZX06`-11Mi*GwDT2IXUU+J*nkdll)vsNDU;n*gk3 zC~05BwK;`q21q9eUfo=pDvwDC@llYrF5DbY3yWtB` zgzi|SaWpn7zk}r{Ws`O&4w9!ub>i%FV&37PMZfjsw*x&b27q6yS3XxEwS?1?omyfj zf!roiTPK?1SKjJZJ}aQb@VH>;09P#}xw36un47B4>!_M`bq!+E&s_0YH-@W{`xy?O zh&9!UrrZv>uvf?XZO>c0r>7%Aq|Az*olf$v>KN@iuqI)4fiY3PTgoaU5Pp2|iZ6Oc zT8KO=0caIlikHi3*XV%;MF4`*e%L9{Zjp~nz;onWgdRKwPyJpZ#r1Oav2)gCGhwL# za}x!njYbkB`y@xm@2R)MB~fL0g1baYD2dBe4{Y?tn=Y#xicR|m%SlqdhHTNE?~VJE zJ=#0ZZB(O9D2mj_I)K`=ig)MJ>W^q__kF$^EzO(WkD(`D7Ukv-aI&7Ac5|9G?>%tOY#RXuIt%&TsMngiTo+KJD)aD951#M?drTa zw?|EVuX~?WpU*D+f?PO9|3yZCWpXo?r3)Z^^jVT=QdZyh#P*fj0`9bi$iB!*8%>^Y z=Ii*f6Z(9d(_pN3^F1)7|3&ujyR#Fwx_KtzRZLb3Z(H$C5V zLI84~167sdm`n54{tUCoI6W#ldW*2o0$@>Q=$o^o;6Ly9U-@OJ6;F9AIB6P%-Vh0; z(_9t>RJ2a>`6Bh(;m_A>YG^X4)Pg3VV(&_D!BnyFvV-%oM<-)?4-^d*svLz->+I)^ zi_C$hcMc1^iP*V@fSxJ(cZN^jVZ|078t8KN@6h=DoCclAwa1ozYG#gILlC@_k6WB) zqR6OjS3vmRykAto5t%ld)H2?f#=f9}oo9ALZV}D%pkcsTOYCN*sJMny9?eS>*#>+4 z=aN;=hKj~At41N=e|;)u=I&3Uei-7<7x@@?WY~o}{Bai0kPtbGg6#RqBf$A?DRs@aS~)$#AW|`x~c zka1EA0=fCTT9=h{0T2?N)?PlYB9$L%?n>eSKm52HSx}2i-^>gpBN%vHX!* z5~BAO3V9D9+=4_3;-?&*_uYT`;xHv>K;3NNLL3xdu|ND+%MSEy?h{Vhc~vp(&yxC} zXz%_`P30cwlx+jCy--Ma-v(&Vt&PaQ_lwK1fIb8jYSN!tSYmL~odCdrprNlLkR3Yw zE&#a9&3na!lm&8+I?>DN?3EQZX{v_h^mHB1$IC}-Y`I{mH0&VALjk*JoA+hB(Qb70 z`yMf;jis`xV{FL=tEqkZoTaIim08;bJdvMXqv?bpux*Fut1$jUe5&A1D3I&@rbut# zd!{hV)#$ML(_|nNhzsEjvjQDc;Jp1ebY=Kn3aODXSR(VxmaQLog{4KRuOvHl?m|~x zX)p8s8(Gf}!!6f zGn{;I>4F$&-lUttTy*m0W{i~zaJa4t*sE>M21seV>dQavFZ+h}D4sT>{Fe37jpPk7 z_fa_KoLyRG?RVjnLGci3(@f-PUxx-|Z{L|n%zjL&Pw%_ddj{qz^)4$?R46m4WO^Vk@8+5L!b4*Z%(DG>$@kX`Zs#_}G!CXjbfjQ8XsV zgvHc$+c1EOoIQy-@sH2pO0P>>kA2x}X;OIUa=|Q4_P}Dzf{F0ri>z`sg5`0MrQ7qi zsOSi0%)gBRCU2p({Jv+Sed0I!h7jlBPTUNIv4ym{(0E7DSR@9tXm(7M6UJewlP%m4N22n3q3e!O1|pt`G(R_V47J0G+Vzt>9qA;GBE>}&{>aUC~NC#2&3 z$LWS*|IFkIUUgjR8fbO+_n!Zv1h=*dYgFkAiYz@31Uz`mSZ6_^AxL zDUr#9o89GpE}ZRh)Zq!>!{FjvKw#bR1u7A6wtR-)tg<$Y1ptg^-s#s=5xXNBpPf#v z(`GtQUYdDz#QzU>eQ8$p>RTTLQE5qWvau#SIG$WJrb)=j@zdl_HR5CY1SJXnC?Ql> zVEntGxPA+!Z1H7Vg=ssSpL7W#`sKU)O4i}YZORVi3iJjUdeccHh&q?gYC#qmW8)UD z1AH#mBAY_wH?^9tBn-sL)%dQi`_p6g3Tp%WttySgTfM<)=9L{DixP2qWfUS_4a7Hc zas}Bbt{O+0z|~_x0BlFhk1p; zs9j(Q&<3(qu)OzEGi>i^y-PDgL2v3?#LZUP`C@~J4Y7AMd^@wxM5AomR;#sCL=Yt! zAJj9=oV4(H(}Ke1!Dvj2=X_IOWr4TsY5v6T2rx*yoJVP960E(_3gqC3c&UO#{Fxye z6$gxu1BOt7I#X6hVd%(YV%{oK)1D(6FsU)3z|QNFW@AkdDY|}X8y>z+i$E-qtb$}h5V|4$G~-nzO|kIR;qLN8Ox)u} z?eaX%ZHy>uNPEDk+j-PM5XVHB@H+v!2U4?n6lm}=74ku~dKrrljTv<-M&G&0jMcR!N$!<01?~&j{qBb7Jp!&<#qYcD^m%X; zz)p|0?|#@sN0;vcGp90iGqbZD>NB027iN=Jej057_>GoICqaS4|6R#AH6Kn61tfWbLk`9mV_qpHJ5r7)R>?1azR~=JtOJ zx9~1!B3l`n)K$1;ans5Nrpl#174g`)_`W}t_c<yo zD}5AO)df%N=c7Z%V^5LT<;3nyUuEorN;OxL^F&5g5ARE7)Ie58HGLG)xcugdyF#KSV_g zD~KaGLrY^iuhTZGLxMCb?>>p^gdpcir1&XuQjQ}Ymv=UW&%gN13d{U1@|-~9(MQ}B|tCBa){ zUjXZ}4lmc`7ZF(!7g=HtPR(J@6y)}PxJd&bzo_rNAKU?TC2r467oPtEpl?WhB3Qjf zAc&riIv)E07~b;oQGv>3DON1y;mHXx=^<{ZrF>g3Ae|G+lco)Pae<59<8m5 ziwj%;@5g5Dg-v>k!|LRTFG5f{H#6$Q{~Gv-f+Ct;=*%WEj@JVLb#q*ZD5>v_3cI2M z8}rAh>Ak(%c_4<^3xMtb4)c7S)u`|ZX@Z;P|IlH-=?cB$vM*#A3z<44jDdcvSHFVg zG~yNMs*kfZr+$#hX%$GJGKAC6R+L3Yl@d|X+V2Vy0S*rR^B@2?-dC*MClC;~E?8?2q0#W9>7p%!>*s~Y??LRG2ZyFF` zsQA)IJrk=79=ipaE4`?yuwFN^8MMGqsu0#WH_Wx+fKVGIN)QNh{jNRtVBG(BXc^Bo zD=iJL`)65xn^xst)*_V(7g`RY>!7yRAjdHR_3krqt2?q zv*EvhJ0%N_WODm&KHIm0XptlfMILbv3p6AQUC?}rcIQZA94O?K|KR((g{S#iybHI& ze1~}wnf{K)jY*XXLb)HSF39L0(t*c=j;E(mb(Ef-FQTGkSy=#{+1J8iu~JXBY)92%M?&y}Ur6(l!^etStMxkt$IYjD zdQd8oeg18Ejiu!=V7wq*ZE;)fl<-he8u+8~Q3z-R{+9}z7(JSW%!EUz9VKaU=@y~^ zum8|5s{+9OEG@Y8%cl!OZqmkiFwA^+d@%SI=|5Z6ZHXW~PSxa_CWs%WjT~FWr+;I{ zXP3PYlZVawQ1vOefKM!w>o2Vs#%ZGmWk2xRy*192}2f`i2)*zoc7oIZqurbMU^)V z4>~Knv>^}hL}V`u>%`t`23$W@pAE%cEp@9nZ5uv^UX~$pjtk%n*ir-hghld0qPaYd{pWjs zB^N{0LL8~u|Mi*&uZ2}*(gq4{mfbBro$4ha>5Ge89p#X*&X9$?a`G{l*?k67DpCIj zxKP#y+6|x%i4$!oD=PYr!3_Yg5)o(s9Iav5`>;AI@lUbT3lne<1k}>HSQYWy6Auvf zOu0Ul&2&6nvcu0jBS@)VzS?u;N-Nh%CJr?!P{HT~OP4^wCJ*-G`HO*&CEJFp!}+ft zdZC-ZhqxfdSV7+zWVhZo+b{nf8cH+eqGEshKcj1g6!yfF!u7!etI}~H%j~2|Got`Em5L) z>%~LSCPKEdqqX+vC}e|1@93arXtOWUJO2mz=lG!9-5Wh~V0ay1eC*R&dN8)(zidwh zCc4yg;e)rC3vN~OPe?`6;&dg`Muv;n3~4!F#FBtlM6YmVezL*)lEZplMlr`hF!rmp z3%n|y=J~JWHDLVJ{k<(CLx1Pp5@<;1q`@-UM2gVO%F@dDcn7~2n09BSY|AQw zg<_(Es31tElI&o(1fBS32ab#1kNmyaQZn!?Xl!*k53RqyqA-fqQ(f{YV1mKt&g8%^xzNFk zkwHkIVRw9XTD4BgA3Q8%npGBCPuAqBj@8x7tj*a>b2mNp^p2Rr?vU0Vc5mpP{+JoX z@9iR%*vI2u<}DFgHNz=EH+P7;rz3t19Xd03rij7)W(RSAUBJ6Ju!-;D-L&y!nt^p1 z`X$QKAd21dGDZ2DLq0T^xR=+u%($i?(G4FL35aSeq>?cmcEGBnYaUIFY78@smd~P= z$czff<8R#se+DB-(zm?lAw;1Z3pV?+!d6=~ye1E#l%>Rx9{I6+beEg(?~1Wm#y9>Y zm8Wy{zs)(t$Nq#2{NxwGzLzDefou2d6cogR&+BFe=pfIdiQPs65uMUT0vOC)cU`D} zz+?@cT8IQ1H80b5h>cZTa6h;Xc;+|etZi#M0$?8i!FAjs;Hq|RER(pFI^WElyXk(r6q3C#7-4mC^rS|oWvfF=35B(HHk zjH~oXtJVE4$A!l5-m(1vR7Q9QfOWV6u8*{6?tU|Rx&K|`xKqTz)}>N3mw+!%V8%$# z)2GsofibvvgqJhvT*t%dsd?PfU}+&$jwbk!k^o%5bJ^~D4D8o|=i8e>>^MNnal9q^ z3q&vZwjg&!T$sDiq~>jkmzVn)_oJTlFmQSby?toG4l;(H`3jpJ6>=_HT5a_|5+Nxw z1hac`gLz@dW6csw-(~YLgMZ+f*s-ZP>!mp1P(oyFem4sp?CrTjNQIn{=~+Tv$yz~B z_e>um>~vSW5xcXdw$HiwJ&$dP-^OAe4(HG1!(F!1-&uA_7g+(5Ns*A*ntD=YPh;hs zV}W^dD2}ubloJdU!pEdUc$e-^Ya_c$-LGH_3B&5Yx4RGfWEwBYL_{A?6O{fnciT9S z?H}0Z7~DM9UawAm)4TR#Z<>loa#_xFSoIOM`2H0i)BMwC*}AuKFKQlbhlfq>N28C< z{*8F+n~du=tg_rQYJD?{GmSKU0YXBi4McyFwRx1`Qk+?)xMK8Fd~8lr(?$7e?Y8_L z-TPM&S_Vo&P@KfyN$<X*FIuj})lm0*U;-+V)QNv=^)6+6-o{rNF@P4taVCy=j40HfKr`_SM;aOpZIs zu=2SWnU@>Y2!r5IZA5D~+^-Li!?hQq-^0jyY^Qb4Okvnzx<|3I)(#C7k7v8CA`ga2 zcNFBQhm*V~?p47ciGF1cbbAa@f!G75?8&-U7;gvsn#5I$`Rw5xXJ;~aN>|cvlAN=o zugF>hJcQ{}8>^dvQC-($;v3#|M_#|??Mdj|N@zdYkSt!BpPzOS_x~k1`b`z|vlVTt zCEF;q^5E^O$+VKQw32F9YX6Ig_OH25wseHkZXm<_d7afIMuK6N^Wn%m<=+B`L{2@` zs&O_P+$+12!{3oyd|bONY_L<5<{pL;oxFg&mR2SKj1vmH4`b32^!POEu^*8Fv+2R; z3Hem-wI%2=it?-U%UwH!;o*iLig^+@z5dkGVn@c46ZPXZ^cX{eKU?n?yI03sVBsf^ z5@3$dV2KEci0}5ocRPG-t`R$OY-YZ812>J&i0=L5ckvV=>*uvICgB|3>kt2)eXpE{ ze*(eBcS_~5BH5M0Y%$SF#KsNp>T0)SQfE&jYVZK} z6L3(1OmL7-C-Fpugd3+1p(E}~I_h|6_`!#mXwn66jjfgMu`joRe4+y^8r3|08Dv4- zTPig(tK3J>DN(5cA+Ne!^v1lj<^&N{C&`Khn**=X?WSS-O%X6_E=(j#zI2hC#rIn` z)hHHYaDk?PovkxTjUp2g^gyr?YM=yobd;S!+}qIG+1B4lI@t^`hzF_caPA|4koQjw z4Y*#vgQ6e_7j`v=4^T(46sQiFmo3jFT!{#E)O`z=t{NRPkv|RwZVv>~D}R?>ZM548 z`K|xC45JV+n!mbI)SnVlZZ?KUAW2y}mTE85#Jui?{xYDeLN-JdB6F~6^D|UxvQw6i z-~5~u(*RoQ${(7kP%Jxo859fxcPfUlRl2=U3`^^IiOkhiFVuE@QqS}?zVbK`Yb!fN z)S(sf$L-wyv#NZ{WrxoBD|$a%A)WoNobe?=gG$pKkDH|v{#s-19ub;L;U8xoYAeQ|U16S8qQ z&m8kU3i5BkCZ({j|LQ2?=FlR*-o#|lY3b~2{qyIQ%gdvrq*38z_e>WMYJc$*XLi8T zMW@8o;McY@Pge_i_K$y7+7c!$cehikRBmKG2$O!JUh4YqL8&?aT0?2wUr-SrN^0$KV_xcgn!if3q)mqrb`E z#tOMhu)-FzA#Ck+p7XmszcF4n0eAMM@Dop5vrMKQEJX>Upz&X~2XZJXq3z%?nE zk1dIe{;C+>(^q(;$D*mHw_3u#FvU0=$8x}hDNfBb=(I$oc%vcePvKvZM1HHP(rcbF2RetYoJh^;zf$PySsbw;!p|{ zC{Wzpp}4yjclVp`oO@^P`#YJ*WY)X)v(|cK_^+ha{COy`Ye>5$WRCsd-ofA(YaV#3 zapR^erP8mgxqZ1c4T}Q~0O7;Nu*1&;z6Ict6NvX*`&~!`GcK1$JM$~__+N&`t_I;w z{OMphe>jN$Ie8Ppgv4C7R+sKo$qL~+TlZE`nc>CHgZ!!e68El7Sfa!Z+~ZYoOQ+wW z{HxwHJ%ab}NK&Izkg;x+o09R2I`Tb4LxV?e9u}-XO0b^}!zIMTQ3D0DR#%JW`Y^ZQ z>D5{9vf0Ijg#=UnBpG>y*w|P!8LTFkgMQeu{|1`BLb`L-ayGHQzhPae_jgyen(6Em zk0Hg2A`b)oR#71q=7pY7^l)O|@mN>cX0VLj-?+*%H&?Gxau2&Z`}>`pVi<+TQ-ztZ zjE9w)qMQf?-!1J5loWk~Ozn&C1=5 z7bL#yMjZ*;n*C|ug_$(ZIyKdskM9R#khOR^d)nEhJBtVi+)qdh?ETZQThqO@Hum*d z+bdwf2o0LFFmZJylcE%U6~Ab@o0F7*ygVfyrCkPxPsL_ojvhV~zH=oww?`$%k^4jo z1O!k30S*M)@Vz((76O}WZWUUaFuhh2n4|wiT!GG{#W-h}_9=KNMRwbz;7l=yp=p_xua%bzZlbb)D+S48L8VZQ?IH*Czoc8-Ej8LN?s|xjAcNQ_j59+1 zn&z9yZKPKpJ5O`Q0YY~nFQ;{~l=LK|Tgjw!(Pwh~UK}BTsdfLYh@62w{4~wC$@RB#KnMyizJ9B~h_Eb(_B&ril6)_DiI?)d8bz;+8Rm(4?0BCZ?HquH7j$M4h*CZANyE7jz?`)V+SIwIyS*q$v{o1r$Ok2ms1}YrlyZjSZa%(Umoc_kdU^>xpUlfPOVQuVn&##{ zRs}T=?MSkHFQ3(b+c@)W49M~f7riB}I;Pk=X1=lvq<9`xB72&6@vgtTS%2C(lFLk~ zh#bnKRf_n8QYliy>8&vZA9UsJ*MMV868-VVq%uc30D$wpP@`%U35>R>eX4Esu>4k*WqQ@X^b_64;t6iY=|Eu?}5Od;Wxg-QT7=>|mE={YflmemRqrjLhftf%Opv&d$nz zh^tf)r9z$rV{cDK%9QC!RA`WC(a_|zKZiVwg;4!#9g_}Ko&M0=0--I%;W9OEX1 zvCzNfwE6mesql(%7LP1bnh$~^LJjqn>9_4AGO}O%?KL!aP3V)YYgI4y2Oa$V{m;$~ zI!a4V>M_v~$MhPT+cH^kMpCS@Rq&L53VZjHdG4r*P4ngyW{L(#lVZe<4$ojP@ zj)Ebz)VLsc-fWLyR=`W{Oc>Z{?BZha4drwBto(E}sr%Vq$wF=Sm9rs%v*{LsJv9uv zAB$bXa~wB95fe{pGYH^X0ktFqMx9udRUucdFV@Y@Wwv^K0I@N+!si|gb8S+uQ>${7 zyC9YOVW_ET3;PoOi01O$eRvp06}0Wd1GU}#<2!MK79StJ>-)&HN+0v(KPx@n#+ia< zH3~0!(z&^jQDJrgEP#?t;Z^jymicjz`0(P*F0{b5!@4sj*}Wi`%Y)eMh@;HY3bX9Mm%0mo0kY`L$rO{R%kNw<^`GXFjY^wrb@nKkYN^ zTU=(!m3g8#AO>pV{uSiBW=CAEv8UK*Rxo+kEZTaI`|5hMeHB<@aUS|E_s;F5*9$)8 zvSN5+v9@_OP!HcV7em()!KdK*&osMjKYF1N0|ap-2rx`Q1tSB3AmJB{lo&t+#A(Bz zc@o|V24Ohcl1Y)QC|mcx4W~bi0M-@WOr-iYwJjV#AOLzlnNP;hiPC}y>7?>4(L^zg zueRWV3Xj(w|B@#oB>W}SJ?|FXmXn!W7&m}kw51!7Zb4l-d=qDQ@R-v!{kE&KZVKm1 znXRq+wNe=m!(u@CT`JKDthxHy?B`s!q>LO0)=s`bs;ja%FI>e60#kjT!ijPe!iocY z?Un^1rz|V~lbrZuwvX@kkcM5t2maB)D#*=q2?Me&40VxeloRW!jGEKw*J6AG^8HDR zwYL0K7W?_C1L_Lp^F#+8TsexPtiO+s@lrKm=DrPAp;L2qiitnlbN&u5Y4G95(rs@W z_h68UQX3y^9e3Xg`2s^U%hJ8IM$%kK~8 zSYs!F-&R%E)jT6_s`cbICj$njf3oVS?MjZ4^Kw`ZM785j`W65%QEW9)oHQo_7`}@i ze-#HGX2k$q3YFRz^n{Jew6@k=b?g9+JD(Dw>4xJzO88Cq4nJ+Z&(4TG zO*(5m;eDAN7}yJjaNT$3V|Xa}8FYmlW_O@ zN_MJ%L^D)`Bkoa*<62|8gL-AcLzYSn+^mz;>!XI5rIH>wFr2c*tsj3zvl-5)NSjTz znEhGA#v>F)VG`pwhBgrUQ?(~yLMs3I<{>5HM1*_c%DLc$ zd43wCXfdp*Q7_vyfq;?I%HIJ*{GLvrqow^>t?#P!qAEYk5C#`*xHHsgabW&rEPIY0 zMZQPqv&@EqIizStK7*p@oz`4+HJvTN`Ar?u%f1nhW2;0>>N{T}wD z$oMAH6bcdj?=O&OVyM+wa7TO><% zf#NPN{kjx+{>KvWs2DfG9&Z;mJkF|e zwL24^{dh5g0hA!>81zxeF}?C3pc8rsAo&tQG?|+wnEg~vGyrTO{ zJkl9F^ezA_HBz_4l8|r)hwNu&^uZ)Xw|{(RSheaLL3pT1+qrOY4p|&bL^carv~a&n z^)2PS(QaRHX`4qWl=iMEE9XfYY?s{eD#ZM@rmVpL(6Xt$rTNjK<4M~)srC%ZBsG^K z{y35m6-oor%(Mjt1WdTkG5&kefxYvdO4^l!1C_v&v z5LjH^BtyXf7x~|2uTDimuB-kd3Rlns+Wdj8)$J)H+!`R0vwK^u9`Pzn2%WyD% zg|+tB;Cbiq$`4Aps2?j?`a+HVW@cVnkHr$%v!;La99Zy;WN+uegd_j{n=yxJHlpkZ@jgM$UeYNQ*ktc5k$i zZ0S>xEHEJcqUv?wujKXJn-M$KH=(q%%owI}cQ&sEO01AP!>Nm|P6@h1K=(YWCcefp zT(*N~(%|09lkmp~uiL{1e3X%S9N`d}2eKRZ@%m%8)l;s>-}fk%Fn=)ZUk!)#HKK$HzlLqfWeCj!R3TWUPE}g! zV1d~r0nJ}(Oi%J9*PoQkswRoh5d{pJ7kQFfNDe3fwrTI-enkDIc%Biv9XS&8?0Ju6 z$wBnhs`fjWKrFmki`vJ)th&BF=TMm#$x2zf?)a!M3gNz^?QG*!QWDaef6R-1o0Z7Lhgl!~vS|$HO;y|~p1p9C3 zzFVy<2)(~5+_MC<*6n((c=lfInBt+AuHDp%f*Vc`_t~r@=kf_qFgL6nEriC;!!W zb7RxjJ64;S44oyJ@ue+2oQ&^F$qa9U+dFS>H=B9OI&*vt*nqTmXggef8s>IBtKC!8 zW<%?{d1*t9bxL4nukS{mqN~50Ol7D}8S-tjYi;yob@5^MK-gHba#TqvHkG07VsbOK zTo07sF+W%-dppY z+G)N!+xvz;k1b3nGrjRFE0<8Uu~M)#gfzvn`(Fj2{IK^`2IU6?p~P$mGB4?+Im3a0 zAc;t^eo3xh6{i}Z9#5}4u&CC{Bkf3muzz7!-oO z-AoM^As^2UXq75!(DpQOilF?{-o%VKRPdD{-5{%26**G?s zQUve9gYt&xNfY5S&V@`G>3;!a|4#lnUfj@OOnD&H`E&06wZrM(johx4t)J!Y-Ru6i zyK~}T-@xhuq{yiW>uFv?cm>;{CRSg!Q(%$4I1_;13vBx3WIFUV2gL)pD z90nFd21pTL1F0YzEy?96|Ax(^6B3RhFY*ma z4n5Rb2zS>7 z`;r9PFlFA_kH_e-%#YzQ z0`mi?Zw{9ZbcdX>o}`d4;?bw-FP@u5G}sLIUD$B6N*jVUL1D^(0LU@XSJ!#Am@6$Y za@r|(mQn3EIqI#ZwZXv+M(a%JcV0rYES2``;~xo%;UE3H0^!O(9!yk%7C%v&V?84chr=&L`sDh zeVUmw!M%IqoYu3ctS@9mM7u3SsDu^tdd%8h<-e|0Y;0q5BIL1Nl`w%Au-@uTetQ6?*@%3S8QPwbYyL;$PkYQC~mh7$}8_rH2?*Wf`EN&eZvX~ zV*b3-b>H!bG#!2`m1%pXQ;GAY|K^E~(D#RA!22l#1Q#GVfn7Y6`!Zv|Q@EmQ_x74w zIa*nd2!M}x**=ihe(iP^6cdXv7%Af1-n2$Rn>S4iS^FsP*(6!^6MTf-3;gyExH7fIdzxA;dkS8C+Z5Xn-;^#S06;BhZaF*6 zT@C^6ee5uld{4kE)h9SO`@ zzxZCwzE(056Bo&TAw(17>Iim6G|=*uZwO`71Ul*nS!f z#mlCBQJ~RYsghwfV>I}wNFsk@QsU|9vAUYN?e;M=WmMOmgI{Q(()y4$_v!T3t-Y8| zn4QPxenY<4*yk>#CPmiQ%Sk@bG;K;uY}8C;{drFAK#ejId382|u@GwPgZ_oRTX z)!)Hj5bSJda^2tiuo5pCZzhX4P<+v7-r?^US#GMp|~v@Pj6>M9&C0c+aomn=L{o9(nTO=YmMa+zmU@(rYDSxkWi zp(<@Nhmx+QJ%4aQ(2G=UW9Xn*nlsr(ANzlvdIW=Zh_ZLho^~wgu~Rc>br|`>7@5+R z$?6DwO+Z-hNXBqM%wrJ&7)tT_T3TPgTN2n*m{4soWm(Li)qSV&f+AZauGv3X8y8d< zeD&#Lv`=33?kL%cdpj3BeNHrl(Ow+-KGzcQ45gP3tbVGXi_Vt; zp?RJ4l{G;T-H#5xD(|k-ugcx_Hj(03nabfI|E1z6+oo!@(NSm}nl|4X$!D*Ob!jUlB!n?OAQen!-tf>bK64psns zfU5LXMpTD$Jt|O)p#5b4mV7PF_Ct3C`7jv3(N87L!Gm*7KLGL~qaz^AiEO`jTE5iM zB*>HZNOskH1xkeE9bHm!PRc_MzT*J)y|!MdF1o(j8lx!1eq-&Ta%w2iff<&nqz6aPGM`*6E*>LRu66vP>!gaOB7g z=^M6w7%uQ7%&d<5^y|d)<8&m$lyTbuzFSlWJNM&^SgO;ehl?=D!(hpi$X$y0*ci(@ z9Gai>G0t#6HegL_khTCSQz=P#`fpIPuL=#Q*UI z_KGoJ`=(gPrJmi^)L7lV3O2I%Q#oZ7i;iZ-F;vEjCvrJM<$uKMZ))N}krJcd>VrVp z*jnqh{xa~orL*4F^v7vy5U!{8ecyX2zJ!R?#-?Axuv|TT81eE684c_-7F{Sykta>L zDODf?YD)TAM1Og>lu9}BWOl3DE>K2)skF(~`vHy4JK2D`!v_ue9C#6Gh#G@|#D&XR zQ5UcHQIRZyeY4G_0u~Z(U(5jlfJ4AykboK6?E5tdc&Y^+%L)r6)OW(eri689mCBcm z8>G{Hpr#xGa*)u2^3qN3gY`{Q3T+oy%s?OpB@p~mZIHvNR_`P2i)FTWjYjc9HFmsg zQ1n_%5u=4^LGT(&2^KsBU_*>FC>>(W`-9U|Pm;#s;wRWI2?(+P+mnOVS_5LD>)@QAh+W^1^P#^eW zQ%iO%Bf0?VzkUnEAb}Q7w4iE=CR=-f9yI!1KSZrLct}xrR}U+d(DloBPRD6Q`B&y0 zbO8as2MN=k57OEn9-p1$RS5wx&L9Xr1p@>>sPyEc$rq0trXl5sgx9q{MOiw__m3Kz z6FcAO=ToCn!k;)GktDMHqMZCrLL?4GU7A;+Dk9Pp1d8?L}RO{rUt6pzd-S zeW}dVAE!?FBo-So8A?1my_caIP=5f9As5KGpMP^ObE57fXlz<}^?!Tq9@%4r%WidQ zzg$#`yEd)W0&=i?^(cx+4~xJ>E~(V7c)c|b^LxS%$3?V1CGvm0If3np(Pivq;{sq; zqX{e=smz19_JMmC4FLcEG9@zt05K3i-sB}2U&bJH3L`IZ>Zls-lVzI7|D5)TjVsP@ z186)$SIF=O^W-yMCyR^1>?JoHG?|rgx+_m_eg1uAJarS$hFnWw+r_zr@403a~ZiLC(~@{kA-J>I9(p#*VX*+8`S8jf0j?NlD@Q*P&a5!+v`K^uU2aB#^@7fCD$!Xz0Rlig`P+@jolCElH5Vn4h+^y|{G8<< z@`F=?ZZrl!>L9it0DREr2IMWYJ110qpK@@bgmh67ZjF~?eSIqwLBLGc;{9_<{N33y zwq2cY$VNh20*te@!MMj5<4AwbSgFin8y`z=&Z1d4F3{J^KM@a`5z%4vz4oo=zDJ@S zF~p`4?=gZ65$XlVn&e=rR=1}V+NZ>3+I3x40TvnpSL-cMp!-j7kI{Imn$M2O<%Q~j zv!-=nX@Pablj8ILS5vhnJCt+e0+%IK*B0MDZxWpTn;jl*f>KmC9V(&vt{JxA!YhGL z6Mzu04XL`%^;r@nTH4br$qYpOp3@@wzhRUWmeexy%iWGPciog@qx-r?qQ_D1UHJl_`rt6@f zP7xAj`6)43rWg-T6S?cIR(J3BvImr{d79VJ{WF5ppkrGZ@4q-wehUJ#wn<6Ri*7g& zzd1PR@3~8hw##okDET)X+>i-37&S>uV`H|dKN6wJMaA#L2n|kbtuDXw4?VBPr~INm zN`Y#jGX;$gFh&J#Q&2-yjkmM!-FE!lo@>pD4R#1y*g>P!A}W*~cus#dZk3<6<8fT_ z^rWc2=KJq&)X~(MS}h$Rn_dlFiq~e{RL9!`0&E0er5X`nIpv$Q`TE<Lpd7n-SY(YBuc5DEo#Oz%m93oi)%OAu=|vqEf`5@}@R+4p(;Ob&4nTt{s8z<=j| zr`ITx3ZQ}#>Bo2I)Zc)(q|)B-)lu-D(^b)*aa?lUiDW5U_`^XIA&p=G$j50vZcPn# zuivcEkYvbk=G=pdt3Ra|6R9S9vxY6nr~au7*uS5R)F;HGbAv=mqkuV(Imp)gg>{kD?m#Zm5qS`qK`^t$u&J{)>);ESq~kk{d0V4FH1 zblYG627xY5*$%F+&Pmb^&1w~3tLx8RWK0y%E?!sp~wBeh52PKg_O zp+98fhdTpHNg}v?Y~#w#4ZyNZ5MW0Gj}4_+Fgv!Xs-9*iBOfesjiqQbCa9_V_a7)K z;ZfX^73&O+Y{WUwskCd=UCXxD^gnwXhPK@(^Xww8t?ozzw3Epc`^$Xg>-!_&)bJI*}|wirZm>KhPR8f^)`+&nrZ3n$&=OR`P>&4Q>OG7cY5!gK{!Cw zkr%Aje_m=D(X_{lqM7FEcpSxVzn--x6?HOrdo6LQYp-wBU{a4GS&C-+m$eKtH2jY& zlI>j~2oCwC-3A#+tSE`OY6!!A4>6HpNM<0;6hy+1_fGEaxTI;^$l_$+gQd8QVZUH! z)hwMd^zR@T1CSxLHf|pFCZ08&6%a0g*h5I`ab_9_N4ZCSCTlAU4zrdu8Uo=55UNA;1Q zz;&;td!$?)V$vAYyN@hmF!s8*9@!Wz9#3gW$nRkfI(uXIiq%>ceMg!gF5G1@@6%nQ zc8d`#Jg5T;IK*3;LtG(yok^W2j47c3SAzgwPYG!lSQzA-U)DH#@l`-`ETtSa{pOs( z-+%SrU)S^bpA)byuz3~umE}E)&2%(HHjK8{iRH$8=%Tx$6Z+J8lp@g-0Hy@CGo9d*Hx_7w6NoK2?%>l(@jxD2???BR-thj8a1Vi6jvy z?KQ=EiWig!1mtCzbGgKg{Uue(B)$klScuyhP& zBHf)?avv?2^mG{nUoq}x>?zad0Y5JrgW!RoEL4O>p!|@0ru;@);a1*nv+`XB<>h`d z97Lxu*au_WCW5qBm_Hqd$Ww;Y1}@mRQf_W&Q7D5=Q859XSHJeAMBhd#PcSTEwQM|H zuU@H;R_f`f=$ny<88^Tsfc^Q z62b8#Ss!+zF4T++El+x@VC#RyHm}-v2eR~XJr(v7l{Lwsp?SxXsMTt>sGY#x(#3*qx%0H!2}2=7G3dNZ#( zos%ce|Hx&sKAjpeZ;T~nFU${y@0S$Ff1Z-|!e(FOA_N$nN#6Iw|EX-NMW?v_^_>Xq zbIiZrAT3Gt|CA#g-Tye!)56HwT451GLxqe3;?B`>IB;@b)n5bA@28pOJ-a zab5kitmMu`yGhj?!)&a6v;D^KXtSYbPg}yP#W?wM-(lB_3QtLgSJh=K{#DUOb}5#^ zl1~Qo#H{mzf+=`lXzJQmR3}|_ev*n6K zB&XKPvwqV_4AHaff%|JqpuF^ zDQJ8HK;t<Ng^r^nhhn7!1&xs`6gt`0E( z9h)@Mes)1Whp}#}1vbdT5$kR0OjKJ~sV7qSl7b5{*}_$rc4$p5#i6MFz}#Y|RFe`H zb~Gzst~)*J({$XNL-i|B!GA|0{&`W~hXxDHep!(=4am?o;*)tbb zJc5F?-(w%}#Q*?)`~)^yU~>)#ad7?Rc%jngcZYFxd+n{#>zqfsjoufgcdqFaLCuDF zUh+I3L+wFzc(k4akjx$R0@@-aJS_@B>{(AJXeJQb{E6~vnALT=D&l2CPJ>oQwf91zW3P1 zZp+#Jp2gK`lpL4t89r-mDA7)EaAM%qRHAk|iBB*ZpiXiqb=KK;s{9S3g1+w_M%14j z{&LR#>*<*+Gi_d|Y63Hd*Xxe|yjJG;$L`mz=9@=n>Da7WPAUPVHw(Dye#*u&#KgM20zF=C<;NV#JK!l zCJ5=D6HN>l2vdB3tq>kSR$DO~vg?*YVdyeUzmxUvNK zb@lvn7RQ9-UzS!}6*!4vcTjeFp3JeDlZ#^vbXoR{Hl_8m^(#*C{I@r;-2^;uFJr3` z0%Z$QK!{uloC@Av;|nn3TNF9#DvRFg-w01dN3&j48fUfIYM&pG9#t;rrS>J1gk07m zCBo7FauJ{8$d_uDV;(%Az=5FySKyF(+oOyq_J3z92%)g}ZM_zD|E>KljWRukc{V9g zw{_{mI1(=|VXl*jMn-uEJeat%BFCW*xW-_#GtqyO2|PEMlnJLPVA?S+sIdQ zeTLUGQ5|bywS;eAc<4R{UOyNz*cf2%W-eA3LAjPH(Mg4^X%`MQi%_I;{N8Wa}0ofRDb4f8G-SvEyRh@?QBe7)H5YX8hcIBjeh zGr1!5x!c2tzOi*NOgDo}vGRB>s;-_vF42V%+U@v7Eafqg9dN(fr(3fd0zV}v10xL% zm?;T=_uR1fVXF)gYda8jVDdDvzZR@aV{0}gSY*ETxG^%d;q&mIO_cqWW!ZqZ;*=mNFBq<-n6Po|e1MtaaN< z7_QCC^J%LpKcJm?DA?C`gz<=dR&Wk~fiv0i{SYIUzE&lxdQ4Sv2D;n|HVXs;?HX?2 z8%>p2Q+zCjsK2oc-K7b=Q^~}&ZnE_vS!mJG4aPOz<;O4GpumcwY-+=iYWp!(y>_>n z+SwkF?M+#H1?uK4)|~26r5p| zs(GR@dP@)_)_G_8CIaiE$)X3}F3JBsZ?9)~JSH3u1y`xez7&kx|F&`E!sSE<0>l77 zL$05*I&-Uv+3=t)xV-=l_J%3ca2f>_SsE|jCa;H#NsHxa?0n-V^gPO(|_;qX96ibviFDulY@tct*l)4tuN|9Y$_5eWd_YfND2PN zua4aIoN(X4;MTKv_MizvA10DgLA#NwQzjY;7}XA0{q_FSx^|6oiSCgHm-&J)+{xtT zLZPm6p&3kOBe0_Wjf?F4ICln~F}VBZj~N?*RGM6VLhnXV!1iLk>K(p@J^Dw)8=K9c zo?z_Cm4#27joWVhUOy!R27!ceyg4%C)LU{*m+87M1{@zSPvquPD`&Np?(Trp2hb)C zGL#bnGY!?G#lM&RcrS)p#rIel4zi~9+M@T9xAgc%d8{oOrKgiA zH}xf8-Tm(aPc?-a-jAK~wAsxL7XsKed^ii zM9odA9?z3V_45t43UU24>8w5v=~4wv=0*!_Copo>Y+ZSr;d{7XtW7BRBidmGV1qOF z)uK>)TUQM9^yD?i+W=$BpaQSmS*-$KS`E$v$(C8g-tAfK)vH!_Uh8kvQwIcM0S5;c z*{jd-nigUaoOp`YwD0;6_W*zhAUmm_R@$ft&jcXQV-mF&s?nNgxrFUy-1trP#OAMJ9}u zDyKfcd2^EG?O5U9LHqT$PGM=ypCvpolB_sD96VfWz3DT#e{*w{am;m}yX>f%?!ot% z&urYu6xH6lW~zX1aiNKhDG27Ct80Q8U)Rg)6vDwm>{yZk;QW(0qTS}nI=uj}0}B91 zwW!5JLXilw1VAbv_iuH#VRN`t7Q#$Y%B@^r834J7vH}7e<0x?8R~p<6&XoN*K0JDx z0`e&fMze0}@SO`YMw;^E#Q@--)oK(OX!7N1km-WYx=ABlVQOi`wi6H{^$lg?^VmcA z9;}w3!T&AO|105K_hMT-`Cl3%b<1Skud#&Bd_HLO*v&EU9C%x)Nn1~k8IJtqvB0Ca zWv320E(ZuamS=TGILC!{{xL_sfeEK#RT;LW81h+$#uyj6`H!Yl;`wAU1s!{0?&YD< z$9&A)hTQk7qJ)FgT3%f0D_>d!8oplDM$3gBbalBaVuNk2}0SFms|KRtQZJJF3E^1*$&NFJ~)G-{5@lyPQ-#T5fVH;KxE?)vz*DI3gwVv+*&>*Vfn4{g7G) z>Mkbdcirg4>?bV5f#87l^KpoA~J@Dzz4DF zdB3b!7t7tCo7%7uA1O$|m%>A+frb&f!}Qzm03hViGKGW!pbPjY)$x4s`r=I)_K_xt zemf&7{xtgL=d7|a5izPFD$+oBa1X*`;E)zAO)UC5+AxW2iNLBZWqYaUFzPQ|WY{Nh zEsbmJg4CLSUbh}n@~|ejJRYHofA)75r3yY!%$lcJ`#<9gj<;5G@)NiVX;@|~CjV>X z!i}31%vOUoH?6!!`kCu*wI7JR8MjH9k5?*Hwe}9a5bH7_U9uhETxZr~H;9D?51X6E zAC$J5voT}3II`SrN>ArLXyr}*ZfEs^I9(GzqcAxm^2< zCZY61O8p>E{&E)zTS+{uA|Mo4!qjZvK1JCpSW1lovGSyg!=;|OXMV@gQ0YAMqo6Q{ zvj=?}(bAfK#4&Z`HHYoC(dtbLr!9uCWS}mz`hGuoZ#?vG$J2N@Bu{4RM&tNX$s~#Sil!n4kVpJ7rOA zR-Tg4kN`A8tT4?LPrA5nQ-3t-R;3jNW{6txy|NcTgz0W6 z`p>$|3awH38~!*Q4+fS*CWP*Ce@tD!?)L-Mp(euvZbhyS3@Du+(nXPrkxaNJnC#Pd z@PF;qGvkXXmlDGVXerAL1ZPk-vDoUMo31#unhn%{pdpYU5C{2}!lN{o5z0Qe!5-r* z00&_*^hDjs*LeSGAJd92)_gg103gi%pxRi~z^-j)r4X*GCEN%=zujlSh&90`S(f>) z6OciO!u;Ev-k5i9)v4XZ%#A7&B+fKdH8HH%S6#B}QSdSu%dDg!*|urL@M`Yw{aESS zOpYzC{FdwIc(ne!#~*~eZDYj+&1AXRGw5>2lG2d?g1wP z!N3?eg&tTRLQ$CIm=FV`gm9BhWrn$WTBbRoL!SHH1=s!RxF!ltaBP-L&ch1*D_bOcY76c^-li~q!ST*msXsPcDj1E@95AneNUDjMBg5H zZU`j(fQkP-y5lj1$8Y3sev#o0NFxkUJZS2V@}fzdBT+9S zh%a31PpbJ);Iw`&Vdd6_i*!MwQWa0MZUp58S!&qsMdssBA8)x`N%DNHT1}UvHed2A zUd`+j13>^G-Gv5xgvv4rpDa8moSlVv36;WvCgVnIq)cPdH=3!*3HDFE7cmS$BM^pq zX{P_h(Bjll+URvV-$yA{=dAbvO_H+E zhn;hwBd>(q^LnB3b!y;0uABbt;HGcypWB9I|L0NrhNJQ|B_9hdRThT|u}*$oLDKO8 zFkp-y(p$i!&j5*m+tdB5#c{D9w%+hn;3|QRtoiootxb0gEj%grc`(TtWh7tOz{bVG z*SN*HqLTmv5k5uR*~Z4j)t4$o{-;W$Pu2C-NTvTS@%T{&j|dM=Y*_d^FUFL|{<%2t zR=E>`ncdTk=^$AhnGZC@DF{XzoMz?A=*KypvyZ!vf=Up-Gd zbh4!^oxg(O*t#U!w}mJ5+<8&UmN@wEHv=+cfUX~-fHP`b^Z^#ciY0a)e|ta)6@G=D9tLm%r+i|(UWCDVBPMK9?StDSqA7no>wXc=^yoU1UF@q@c`AP&n_={aPs_$S$D=ZY+Yf5=)?nx5I2o^ zn6D86l|(bGPW{uCT%}&KKr|Q?2#+9q(7UscmgC8eg_2;gljgU%tfV1%l4eZNt}$+o>7&~8YP*;yF{bJX?syx>B_<|)tkZw&^?zi2vvu{whL{MB^^NdS zCMetUkVXQPR{d+gS~FGrcJ4=Rnw5tD!SM2Nv1&&*qob=1^jh|{oo%?>V5rSNdbSRX zhd5kq)5Au6AdT%YBqSYAU8LxnICy!3|5j}5M*Zc?E*=3AvSI&85fLCBK?4u>N6+50 zg&_d+%ZceHW=wEKRvcmzIJRE>SDjwGArSIBgm28w!WMW(8V9T8mo`y-Vlohv^LF@V zrRyL<`oXSiYDVWD#j5kni>IdJJBKdZNDz7ksl989vxUIp^FeFTH=jCT0+r~x+g`Ee z#d_O#Czc=Lg_As)q>R}yUv=30H-~;QAvfVge1xj!YTM(OP(D>KUnpEEU>nN z*jeN}%&4se*Pgv53u6qpzQx^A{{Ypz-Olbl#Y;YUU-!+9k^9)ia=Q35=d!PHWIL4` zFH}$_Rdn>MG9W|FPnC+T2>{9^{4IzutFs9if&5-dr}+zZEXtCQYIjPmnaUfpQVtHebOXw)ig&XNOr!BfL)rA}zfrr1{s;{+LHT(6d2s__XB zBA;lVc*M|B4!@{))h#TQHX*P_qA1%{bC$&xP+1m*cX08}p92cHD29rpTU`IipAX!j zRwkF?6*#JwXyZlG9gdWt7L=GY?h|p)W&l6zh~3iu=n}3H24V*eSvx%1)J>633+K=2 z-Q6k*h{Z_b!~>5haK8))aVcPl?=+G@zOlax(OMc(XQ-Q$v`M;@EmMUtLL6YyXG@UA zQLI#fCrsgMBEx~YojLB?FniqlV^dzh^?G6r<}&Q+=={VfEKLa%eOw3zgJRD6%6LuG z?Rd1WuH>F>Pd#sr3yVyZ6yrWtTdJ#%>9;mF*Doe1tYzsh4ej#p=}ixdiMf-w8(J#N z&p7aX|I|z7d2Ui{%*0%oo;pf^0Jt`rn)dGhKTMrfP#atutpkM;iUlZz;sFX2DDF;h zr+9HK?(P<#(BSS?+$ru(aVzdn+}+{i`{$gQlPfMV87}s0S?^lU;*WCSEjaWz-QdN8 z#B59auB(d^ayHm`$}denC$mJx%7G8XQa>(mHQxT(x=nvMpFIuK3KEM8ss!hry2rjy4qvy>+q+F7o@UkpCj&FVtP0EWbFOxP>J_uKj} zbG5`_2J$g?$G^fGKA0}&91f9VQ^^7W>n!aFTyyY}Phr?s;hvJfO>7&gw8U>EdZIBP zVUQy&IUp8Fh04QtOLPpsjQsb>>Du){?FPJ?y!N`ipX?5vFmbjWoiF2!`-nH#x!vvT z!74RNhY1x@ePn?PN1cs(CvSQ>E_{T509=Y(DYj}UrPwiidX6PE=3HW00&2+A_r9h_ zT`oJ9q*3&n7sA5UgDu4!*nbT%7i%QCNm6tfftf_J z2gS&vfzeeEDhiUlh}}yMaeNxqnBOy6S}Zgwjw_BN>k;DDb@d-7|IJ$6mC6*8Y?7Zsji~R(qMgeb(1>$Qgr_H}%<}W+*%kW549a`RNOlWr@ z0}t*ta0l(Y8AZ%(2H2!GAKE8n<}G0tZHcfG2!7rE^@TA!#I1 zfUNoH)O&xSdyH}Pdtcps2}lVL$wEBZyJ_g-+BVN>t(N!@yP+zlzg&cgMOXdfqC17P zOR8I03KPl2v_;46Bc~}br09=w!|tYDw>knS9WBWL?)y+m$d8T$LsKES2=Xda2-&7B zOs(qNKGkPTes0c=6sD;R@1xOw3cOktYgXsM#Y=tE+y8e%$+{(iE9=#uvqW&?c$6o0 zF38aCJNfL`(Gsv1Ni3hl_`rFoC-x}P*rB<(BzO z*}Gq~WARF70zmtziFPbEUPeS*T zG;l{vtW2Lk6e2)n@Gj>KTWUc&jkY8hN+k{FFDQ}<@w3l&)HD|#o*v^h_n!tXmKJR$m1@3c@?7K_En!of@)xyhpsvd&0x}W{8v>9e=t!bH7%>mgnEv zHi>~voj*AJz%a73O@C~A{_~0Re+TM?n)P@eSr|cqB*PrmZsfS532Ec)I!|Y!`=NnT zhZ>gk!#YtGH6}c(Elp;SJ(h~WURPRUow>7!zWz!-?^Q8rX;?9$wDqp6rBlm^D+Hdt%$_4+Dbu?MGFshkMWx-h9>b2D) zWB=(75*TDgTEAY%2vUhzg*_a}=4UgSZ6cscPgkj5;vr!JvJd;t7$tF5$J+lv+~Zl{ zCW>adQ0HI+D9bzA8VX~0K$|aLtI%Q|gs^0o8jdw8mc^&5YT|8T=j>X2ACVNU?_;Q; z!Myci)B8Vr*8o1DcjseWE&_nD@eGa7S57O4t_SU~e_K8X)u4$#ccZnzz&guNCEJswCwMNHDICoR9jv zjg6fB(Qc>e7T2S7Oe0T%=`k$7R;f9;Sw$ilQV<(Qe4jpo^?A8)Tos2Z>cFiLU;9<1 zOL!FqYa~T;>JKrHt}`N6Zdz?ekdTywMaZc@h(I^FQjNI~(XR5yuAII3NJMZFP-3E{ zoXPik!Mk14uHII+lAkwZLI)@fb6PN&-$q998GjpcMrQ$Q0hnv8R1P)G?kiu7vpZ;Fez+(K=CfmC+}`|xr&!ml3=#Y9hUymJ zQY2BxHwFz=@g~V~1jwHv$@d(yLg^n@b{D@3G*EMw9+eqTJewcd*plSi3N!6mH4$!6 z6v?EB(SDfC6)8X5Q7WoSvq>|Ucd5CX^yOd<8BKKCR9beOPq;5&SgFww0r8UBXe;oS z?b68ycCj(xlk%oMO$4RjkG(&T*Pi=j`ACBHha3qga^e65p+JEC*w*zHbtm~MGHoH; zC`gpkH{^h@DopS^9Vbm_&+YLz<7w?pc}dB)SM*%40fn2F<+!#0@`tD2hh~+U!otwC z#3i<&rUw#fBUbu*Z<8A!L9oUr{m#oVbvgiwn^P(_#g-+@^?g~c3(2J^YHPrHW;;>F1A4^V(?Ss_>8z;I_Uvs;=r)HL<->l892Ez1d zv>)Sb)tBNNGHTAWdLpa+D*~cZy>}j#tI9HTE%{w&qva2SVqq*-NS6cx9D>AsXYg&( z(WIH%+k}s>rj2Uu5xlrAYyhlOKTyQFvUZV!MFXC$A*Ocx6e4@5U=en08+Khq2oPAk z#NrwBU{71M43`!14E`A+Lj6I61un+E``vw3sRe>h>(Y$w{2yF+(!RvgY_gCK_kHK% zWLdUMQCr$SJSE|;J>C58d&g-h=zEL1?RPFr^HU0oG?jaiK0mu*EK0uTh-`+B60 zUc;&ld%b-bF66ZvZ6$sv80e9YV$n0tHSb|P=!Uv*=v*Qtg zpCCgso{ahj4}7h29Id;P`P+4E)3}MILzD%-1b|sIXNM)V!M=N ziFBmjj)BV)cQOto&hHqXbpR_k&#+FZzWr=;v*WR+3DTGTF^Y68OGnf7R-WpGse_GO z;6o>ppd;%I*^9}2lwb#AGZ)Hve~<5DWEc0TUeVx>%pXW5s9%S4qna`YkeL}8CaXW2 zf*^s|@T(9=Sh(0gmEWE{MVjuCzTCm-yhA`x`lPx)fxo+W8-+}oik_*@Rs=v=iIV{!7^Ii7F0jR8 z($*NZ01$O!%tF|9cinVkPWyh5bZehVwn{Fw+J1Unb-yyF6U4)45U{IT@c7P#A0IXV zg{&LEfjPW%0e+8@|Cq9VSg=~A9zCUc*bEGDw5E2kVt=NN$w)2Tk0$S=ZPhYoa&TqG z48nQ2x|$!AiQp^9N7@q382%<|f%lOC@U`c<3J?^QY@^8%*0~?L=@X|WGW<4~|DL5( zZM;&4Myx~&?*Dv1e2{1%R{K?HeFPIjRGGBbQn6!Sv?7s}`12hmOiR9eoZKejLtexO z%R8AZgJ;|DWD{1cA0`4)n(j>pTs_$=0;?^){fwBM>}u!Fi?QcEMa@vruveRVD;ZL< zeXuTUM?w0C^?Dy=y%b}b;uMJ;E(X!o=3BZq3jey(IGOEhYbZW<-GgE_%T=#2%lY8N z7E6_I5YhE=R zB9y_>cA9vU(8a_aZmbf=Itv-364v(i30k$IP=#L%Zz^{z;bVq2(=T6;AVA7JPxKW` z!~FD6mzg{Yef#OpIu@WG1_Q3Q5hjN=EvCG8%FA+8z7LCwaaaQ~pQMxW>%2KM zPL9%GtkgIGMe^pH^d~|%91QhwTtNsIXFiiccdhS#_6lClcLpDprkk6)!mx4=>?=6p)4aP=x2kLW=fPGUiJb8v4l*(K#r zJjZqZ{y(?!f6C2t!$Ksp7~tJn-i9PC3s5-4ALPn6)JK#j&|5B`D3#)F*#3Mbb{3X0 zv7fK7v_The>rM)7Ed8bh03vNces~@DiGUha-Q~!U@Ge$owLtz^L?m}aD#l;M0|D_2 zkC&y|NZl2BlVmEqn5DJ*`W-^&4P??5tXBlUQf`t&uF}<`NL>q+pRAHyF@Pb7N#)f( zATI>^>1j0L_|0>h7eI>j#@=t-%`N=VDChDM4Q>L7(VGEN7)Bv3?&R6b3`RO}^l!=^ z&8k3P4pr&k%i!|V%*nsA?`_xgxS|+K5ykJek6t|tZ^%f}QzL`7nyGQ)Rq=w99a_!= z?KHuVn?liWDqKMGK$eqXazTZ~kM)GQa>~%}6*_5FoP5dAvcXet;_~R*Qt}2uR7(NYHB;_XmE~gm^kH}-AI%otn5pL&Nr>`w{>v?STtTW zm2Ix=TBW!Y$4f1R3)Y!U_Sa`QZG93;^)}z&oY^IpRZ~QCu(J-n2*FRuP?X1-{l7zt zvq|kY%Q-=8B_+#^P00$>Ta)H>dZ(%HTjyL_a{v64wF(f08zMvC7M|gnE7yWxe-$j; zZ)h}e&$zCcfEW^>WpwE~qJILY`0ASC>V!@KRc0vdOcuz6p zk?rgBjblQhWXiUCW9iZPBQ68@&+sq3KXtwGo>AQac;>Bcr2!h8?De9G5QI zA1tP%LG6C!uE_q2|8nRiVq1cpfrf%NFWMmh+dxMQqRdUsLd`$)aTlj66(V@!zVkba z_#JY-EEn&ZcMgAp`mly}I@%DoX~U2`bD+#^>OQGDm^`pz02#mfN2?tx4yJUaZp8-~ zPACWx55v`^nZr-Tr3AyCwcsB9*51ydIkm5+*KQyP0j1b#+;;hYV=K7jz&pAN$$^pk zT7||Ji5#dy&5TqHI4@$>S#^PV9kw>{k!YZLn+|gGmI<(sc-r*Q*0yE`uCBbwE!!ey ztuZLx6haV?5e-_L^EBB=G!p*YazGmdVj`kdrD}BAr_cE5n44+Bx^OA-={lG>lPB@g zSOJEL!d{IuF8MlJ#NWG-AXVzFO#@`f=mn6*iCmmO&Nm`>82$O9CDZjE@w^)~P84Hy zcZf)lQ$VastN*H2sf*%%yiRVz-6JM&dH-EV-2yA=fow;k^1W&YRcL~s{C4V?03EGv z2^S{oTkJq#AS2SIn|c_qGuMJA{#agY>_bW1_q^b)u;ls%VA)n`Z z*8r(9M}8XPXW5Ihv(5yf%Z531M0lO?tV;xlwV`fF%$_!F(j=w4Vpk?1xI zf5=+&+CH; z^aq7Q15+!)v@o88TY7ZMHy(~QpJ}kLFt2P+|CxSkc8UgzrL%PvkxIYK-4~`hS$Hd~ z$*iu~5<{G9&wKsdBJ|IYV#&ZuWCZeCCBVApdq7oc<@t&wRW|0YJZQH101NS4O`GQc zBH#}aCGh^12?eoYIPy5kP0sbjQ{X+7oy!y+zyM}xV?|4ZW?8e{2!O!Tmy`phmHM{G z-Fw9G0qEc(^>E;ZN7+)QKM3}A#j=%)M&`R zBwJ<_19>t_=yy6jcb?DWv$2naz=ZWrF&#&piqgBO(T7b>`fQo=9}3J!h(1T>BSQDu zl;Mc9Z?-4{BnG&BY_y#P3^u;>{Ap1Xxn;0tmNO_LT=s0zz~A;k%azmGj4b#E{_*m%Qs` z>o`aM)h1V+xQ%>_-^Q1rw@Bj^gxVZjKvwb+wYib2u9-703dNtl5B3!$omzT9!dAk!EZJxQey^i9m3l-)tQ zv-|kf2-motua8sVUzV5E_AvcSf`_1meD#45OBv>Lyi~yq`O&NIb28g~1z(vsk7hNx9taGynQp329ROVw3TCP*!_ej-Q=_n8&HE zwsG>^!G#nj=OTV9yS}sa%U(?NkAg3@>|1l?j}AD(?e!}j_ul8j28X4eTH%x%jT< z+fSj?*HVyK#M>L~k(1iRgzmAYzM&mN>M5*rj1dAnm}9@~_9F!pB$1qbYuorg_c9uW zkVi6z5kAvKk9qQ;7!&$2Y>bGE66my-#{Ohaf=dht>+HCcx*!uwkN|;@Ffq&vdO8H= zlvfw#<$&44=1qIGUtGP}K~rZ&tL;nLC&!}FU=x^r@>+X)EQ;_vX2PddH+D*Cli!*H zz_xN_;a~!}Kd(=D3%Gd!k)z7si(69JW6nLY4joiWw_yDr4fJ_{&h6X{cakJzjzL^R z#LlM9=F6+DGjoVlaxEcsd{|;%hihGXrDDY1d=4ZjXFE*hCuSdBim-f$QwyiD@TapF zlqgcKy)Z%f;7H?UXDch3qM|gLEx!)dS3kb`+uzg1NtZLm=Tht+G`cr%^NIzu+zXZd zEOCPPJ6zw$lrT!yo8fbPAO{4p`Agrf$J?}!9X{g5ELb}6l41)ZnWFM!mK49wg%y+# zsEieZ*v|iSa98{B6WY2iq5^z9%3eK-n8XsTn-9(hkm8R-$X4s8&F~ z(X&YdkUlJpY+1 zm4t}$YW5Ta^=H9G8~Cp6DxSZ{fBa5 zSg3;j5`36w=?Jv>kXmpE5xI#fup;SslW0| z`w)SH;G(b<$EEzSu$~w+E}8N6UQ+HUTufT4rE;UuK#+ApYU1&C+#FF{pF!cNI%G(L zWevd=EUbZy9qFt{80btwM(?t_==w)C?d=QM3i1cY zE#Gdm?^C9Gq$>}YuWY>s$jTVmQ>33{2|d?M9bn{|Zwg>`q$>jrX=5;Yr#z{^pHl(d z9DLvNri$?*B#;f!!H7b+C(&-PUz$}!^#A@hP7e+t>)M7||=@@=G8u7MQeSddndlS9#_8ruJ?#p5lOTM>jU6maJytSJoz5S14Iy9 zPP^Nq5lk2{Dc&NNKCr5n2zmI~r>yjKDDkpGxAk5zYJ^{4d6@hlE{0a!v(Enaw8LjK zoZ=xJOCm14du2qHA@iJt5lqETD7UJd_|;_w0Q7PClvyd-|FVs)BLdaTU5(QvHiSFR z&nX&QZdg=#l{-T^KSI6Od=Sbl_<#^x|8x7jjdo{Wk2q8%2K5{HQwwCiU}YC;?rf{n z3afY2EeVS+t5?Fp`f#FK-HsCT9|8@N`~K1VB*m<@-*D!*MBavDljp%p-l43OQ|bM& zjJ#Tc>yng#s?&z(n>HrvBlPQAhVn9LDBLq-iuSX#r$&VQCsu|** z9}5su6Pe;=5-dw+a$xcbuy^F&ckw?-du}N!HdYkU4QXE@uF}J4L>HfQ=WY}t^c%e? zQ>7%>dLkF<7q5jGrYpAJY(TAChL&Ulq~ObrsbU3$_!N)zX@#d&tV*tG+cOPWn}Yin zeK-S_sc{Lzc4@g$-4}oX+^5bAU5V=DX?(SRnzGJQb}~WrSu@v0%_}$ZX{2%Wshjvh zhyx&q#8<*oxptMEb<<;7@uNMzD;}ZWl>t(^^VQW_b#JnSz>Ta0MqL@oY{aU=E}~z9 zO9B&36|l?>zF4hk?$9%h3cU#P;>}b{JzR@hjoY`XCCpu3JIBglnT~wleXxvFJ{uf( ztQeW8_`=DM=KCNT)R}-khsKzn(7ceQmgQUP=M@^F_Gx|?9bgiTA8NXMY<@;Csnd4gR=lIG!YR(c#oo+dM5U5K$U}Qb>s_+BNfKtNVJWsZSYWk{ksu_RX@x# zU=6+^wFGe+Z>Z|K8u=L+K$O`jN`b}zKpv?J$$)i (GS+0)vJYmt5?$qzDk>-0i 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%0A}5;gfvLR&&G*H+E~kVTb}BFB8cb&4 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

ggMF^O&`i`d&Kkk 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

yr4&ZvQr2@S4Z|8y$Nb z;>r*c)3$ZP&@{>m?5>cG&idvH#rkx7czR$ABSaa!ys=R0G=~$JTf1wPWBFu{SN2rf zb%fBt^5g(-?+Tt$%JUo{giy*ML|@}>2ppHrjwXNR!>9XV0m>MqY^TuBP5WEBE%Zzf zF${Y}ayw`4+e5@JeM_|kxXXyX)6saIfByNG|I>f^AGYoMZ~xmre&;(M9vJA`+}!=Y z|L^~L>(ECgW36b)ZXMgb*f6nXm z^uqIO+v)F5|LcGKbID}l`t`-%_>DhUTwHzNfzvN{-h1+?r=C9;mIpY%>zG7@kgi)7 zFD?VXdh;M5R1mPwCj$UfRfiB_jCxinw(THpnc?<1oCjum9P_ zX13)p1_%W%rLN0d3LL@+0q!?&O5qUFH237Nd~;t9D7e%ny#ew2> z5+~z1*wEb1Uu%vh1zUf&p0wM?NU4kLJUHR)3ToCG18)~&4AIxlCS-9`} z-xuyn_Nui5GZW=}wcpKdR!DAry}Y%PU0!1VV4mk7A`EO-?Whk&efD%Zc4My?9qZX)(kyU%NM6o%Vt$XnamS`I`FyL^Zuu0x_t7u{9fN-CO!V5W{@{dHz|a5; zFn}PiT}A;HR5(Hz&-27_?q1;;HgQ}U_VL}0Ti4XzeCp*Nd&l&D`ssUDx0`?X;+j(I zP7Hh3w!4o^`Pa+lUc>&0`y#7FtE{>bj}!qKCTIaI5%6~`GbHd{*R%qWLa~(a`AW^^ z?R^e}7=n7Pv~$z(1XJG7Y@<+Yl&dc-uMW*j-2a}_ip4Q!^O1Ltqf~tP?8V~dj;KhO z=fQ1X2@I%fI@ARq>X?G0v@MfS2GE;HVLcv~(zdSOmgf-q$(PRg6^{tO<<(_bl>Q{2 z9ZUDW@9^P!#>OshZ?6?fBF`ZRDPt}nhX)4Atyb4|!?9SWSs}zluNd;Z>gAxnRMd$eC213Ot3~1X8egzczb{A)aa;J zZ zOc^^Z6;KKhB*fjmersfITJN+yVIegxM)o91ud-^0qB8;-WP_@A?}fp zL)CWd;?Cy-vA**5^3@m4AHMgvSCL@5UT$=E^7hHu=mWOpfsaU&|JXTYC_~cYbu1%8^R7US27D zP1mX8Qos*Q49rbOD5J&w;@PjhG^a5Nqu>7RKY8r22V`0N zjo z#@yU=u~^3VAo=nD2l$3g4X?B;LC0}NN7J!bP*uB?N|WPow`&-N83-upbZl*HpX0FW zl3>uMC?X+5mPMZDzDs+f1ES&W>_MpQsO61T|B)!80C8~lTD7p;KpgrzF52w%N_1;x zuuqZvqHDPjLBIfnz-U?Bx)~o06jz&^fWPO|r<5{*hv&DtYsKJ^0oigH1mK0W?l1jC zp=%I9fbf;{sk^6YO2KNu{M6+pkI_!es&`$UgYd4XsvbwcaexPJUMu4eG;GLY=u)5v z5W_nf|L;JQ0Sudt#6<+*Q;SWRfAf_JV*q2Q2q@<`7`^^nki!Uo{Kj^2bV&AiC28q)f}qrPEvJ5RG_6~{oBQPtpGZYK&X3&dnM}x* z%Qnj9Z$4kUva53lP9&vzm()6rM?|V&URd8eF*-cc*SB0K+&ez1RqGc!D)tEovbl_u zYZ!{`4#xQqZMv$p*~b0DeT`C;QHD9B+higs4TOdN{!B$R=!28KhzBzWEStQrZmX6j ztsvVWk}sflG>C5NAP|DKLmiv?yqx1w2GPCaflj;j@*dbLz`4P`@o`-4wmr71nsgy+ z93NC51UofbcW6+?2muCROV=}zh~DXr1$>{qwAvBmkyz9wOyoG-ex)P@0VU8-9YfvH z8{DA>PMmz-6Tq-0-}7k2vJBN2nVyd2ii;Pn)pxT%6xf{#6QzRJ=h_BFK(k3emM!W+ zQG6q-h7jx;)+5tX%lW)RNKfy80PslCR;ff8yVg+W!21z#wxsZH0m5a!-E3Mr+sAgtxGKX~5}w!WB}JP}C_xVG7Z zD0J+dCZl9`X{phknwf;H)^pqYPt1*feYcj0M0WPKH;RS1(Fq$so^*+AVqRjH^9!hE z*)f^l+|!2Jf$1sV#iGC6Re26Nx-A4F&;uwpuBl zyjU%m4EITP9Pu~y^Yg@7hY^xyx8_+UH$R<=TXhMwwLn< zC?`X#<9IkCN}?wgy#Dg}ZmSa<9EFJKY9~259P#rm*W`FEzp*v<@csU1*zI%}WB?&u zRqMqP=3y}5ZIz3zO9tm=e11S&({`D}oHXS8l-Z0R2u<4x$lk+)BTucY3j$XvH>dXZ z#wSJ>mZ)u&1%ZF|a_$G;J?i)JUBlhVRnoCwd~$Lq(WkZAS+(p!rY{?|ZrX;$%Mx`Q zN@?HtNGK80yWQ>UH!ahS4fVhK@gc3FFRpbJk4P+w^Z7*)ADQwxT4&|r!p>g1)+T}| zoS8DhT>0EZLv=!f6QOc;Q&gl-DDLqGFwgNaRKk$sIa?z}$Ap;4K~w`HwLs@nWuCPKILx@zm8Ec}N)bYU zG211xBgv0{_)IG1?^!23dwKISUtdtWmM0*;#gRw|(@hs)_#ISfbkqHI)_znFg#G<| zsZ{oQy_RJWLc85AA#SBo=QzysoZ~pE+Qk?%M*V*8$jATy;KGG#^?E~=B>+Gu6nyc; z3qSqSKNJcDCniQNUc5Ry-0$;w7Z#SUUAq+yhX^5(Bwf6CwN`6{!=YZGb!uvY5K1Zg z(I5TDhd%Tkj>DE^r_)JIGZKkdG#bff3wPcogwS=}&CT6zSD%;|`w#!&mlhV5wzu~$ zU%v6BFFh^GvMfsnxA_4M0KSESiKJ4ILx+YaWw&naI*!Y67-O8z*Aj`y=W9WgN0XW?HJUvoq@wqIUrbF2V(fW<=fq}U0g3)|MFA0pSUlY3h`@2^N-IrFal8@ zN50YB1A$H0&78>qlQ`s+FugnO;!XqnH|~d_cV$V?Z?|oxmw0(wivYVO8I6hK3F(i{ z)fk}+Li&cKPp?wmI~}1E!B;NRodr5$03lx}IC^wmR1~Az?O8KI2#Mn$N#aD25+W)x zdqePnz`>?YZtT_OGKq+%B=gQg6aELs6~mzb!ojHj`PI(Z^-h0SlzDi4&*V8M@xuS- z9gn~F$l+pJ-6)nGoSJ-gdFArv77~Pqj-(_7^Q!vvqV~jLgaMoz@P6TH)nSMahob%I zcC}_|hF8Q7PI#~H>zYkpT<`qa*D62zP-L&+eDzlQdKS-SVzS}{{XC-{qtk`2us8?; z%VM@e6`A7^eAj$HlBDaq`U`72Klb6ipZ?H9I4E{C`*UB;{pugC${am5C}!){MoE`3 zbY03QrG)YrZC9#Kj0^&oWNo^#v;NM*^HY7vbL;D`VWV8?+HN4>E3E9Vp1#>q-Q9I4?WL3pl5BKZ7_ycI!d~yy?4~CkQ|pbp z2O|W4Q07-;SrY19jRJ6ICr2pFH=Bwe2pBKd+ZD~64*Qd#;O8!1wkdnhk@;08kyuQtaw)~ z``&1H@A|FGi6aSLz{deak_=83IGhkz&1MWDL6U9L6h+>G2m@B?n6;YTH=I&b1}l{{ zB#p`9XE*W>Jv46?tjhjYFy(QbuHsL2TP;hkNBX1G5$mN&DjHNokwI9f*OAqU;YeQ7 zpFA@0zs|OAEG!S-OQmo!5$+r7^Oh=V%njR$c7As~%qz$f8JzQ{YR2~Reqws0n9p09 zu6Vt~C5GDQHk+j_5S<-S0^y-~(kiMu3rp$A@!Y~q$RGgF)(zR?+rD|#7vn?8zRLdI z{_{8!fYAN`6uDp^d9Mddt?^cEGEQ_m6laxuJEk)j}0pyds@B za*_eCQP^#FJ0dSI00^Uo=8O;d<|h5kx-=7-5Ec9<&;C^`6Y|ME$8cUpA5ca`o?qHq zKQTVP!{uB;;CCdx#2Aa5U|9Ayd4P%>4%${3}yqsf1C7t8SVOBdMOI zul&k?E0rn-qJ{$;;G44ykx1a!v2mW~u3cNNR+|VRN*Tr|m#ayVc<9jJp+m!#<@8c! zcX!K^ll`V?xh~;(?!XN4@5-LGZzhsoYIJJ`C*Ypp7Utmh#v+F?j8U_w`D3!)H1AAr z5JAUsYrAT6D6qd!;W_SmanS|=ST30V$7l0AhBjd?VHiP&vTt%NAq0v5Aw-lh@*M}s zvbQwN-+7meGDazJT;dQysB7Cg!|AYaxL;WWYb?Q;E_DcCjB${j820?p`9?a(wGGnHT$u+Sf9%m;eBXO9#x5bo^z?5$^L1Ge z!hUb5r3FYg;0b>{=jla%Po(&-EEt*t1dgfo239+fWUM0@gOcf&z%wg`>C%{&Yn$$K zE8Q#m`sc1S`hr0ZC(exX1vPkn*HhiAL3CSaf)H>hvk5p96C#JaZ3Di%Q7_fCL*xFB zy?1zMP;7UcXq5lH_YZ9C%XX_#v$T>)1lcQc078g8UVoIsJoaMr;P}uL!%UA3@rmdb zA@3aFBykC;|{c=1@VBgidW=96p2)A}+n7AcqiYhVjUu>2jmp(sll>CUXdY z$a4@t%9uys>xQ|hNmzDfyz+9R{d>>9lne!b;PlD+CnikC{>;L%7epzl+dI3v^OGl*_p67- zrZ%&?RT1|e9?v)H7$GEiBv*GVNtFGzZE}b<+FYkhR#EyE?1ju~&(cLa25aCFcgdU9Y!r1wtFoq!zT>NysS zhp@n9CdX*COev*=002dgRNmXws=~y{*~)IdvA5GSo6+=8ax$})-940;3QHlxaR7iT z>x--V>p|JC%D$15XKAMr4=Xzhx5kdlj-Ncba^d=O7qUP3vE#?pNS`NK+bnj&T(B>= zc5}^Wwkd?bvg(CWdVJK$trVyIc(TX}Kav1~yR zV&g%*Wwh5F;BrZSxPLswDBG_VcdPl1p%F?mE$Q_9$*b3|?&WqVV*<}J00^Oj@$>_S zX9rXM=dZ1t8a*wF_;g}m&NSQ>hw*pdCWRpsc~Q6Z zZ}KE%0N9Q_-9P3KccZYwaU5ZkQ96?B->(!ALw{G^|k9Wqsf2tO2-6X;F*i-pMGYc-L-g(T;jfU4Ws4IN>fjT6_?O&&kquU zVqO1sdA&FOIfQSraD@<=rd6rbAcTQ{uUG0s2oXYpLBAmIhGAB#4FGVh)*!@<#lnu` zc)cDSoj7s$*s=Nh?mIC!m`SJm2yy@UKmRuegYy6f_$CQB!Jzl#$w^t3Zr%R>KqaLTJvFF#EAHUBTiiwU(5CRy% zy_yw}aV9K&;YLg0p~MSM%+I=vX{L!Fka#ZQ^;A3R>CurZJ3A9e@oS64c31Q#lj(@q zHLQxtICT44A&yhd7Dk5Rr>3K~_M58ia0o&Kx5{>@FK#4;M)m<7A&}@p21*I$NOan*^3t`cJ{CG4L6}uXN5SbQ{J?!-9SZRMq^5NP_ zwLj7nc`+bk=m!ZyGacuHVc$Z%smUJOFhz%K7xR}^SIKEIx>tgavc}*93`&R>2jDeZ2S7|?qW83qqF%800961NklY9rg)!z4MXw}nl`1YFJbu0Wn=lFia48Kca@6a2 zVQa@L@JFNJLBD6Ess2BI@wt!Qd-_B7-1|Y%b9L$F8lnKOLbL5sA`3$AXm^WrdU{}! z(6znvUyjmBZd>NchTHHSYJ#4Gk?dbyra{qnX@J!8Rn!H#w08 zVllI0SRKkxSB!`ezrR{iiEC5J(gTB5tNMGlw*J2#`S7p)z(-BubX`Ic#~LR3=NJBT zztvU{&WuNS3*6e=QR{js%ir^n$81k0m=wp@h#Y*8G+V48sLb zz)0_C5TWst$98Vq+Fe@Zd4BeRGn|0)yTz&TXeI#u==U#oTb+1BI64ze^o6%-Vhas; zljcAoVp=R7!K0z>g_kx~HrpJAGUn2WfFLs7C;4NXrR5f%-3%p%<72Z>Xd!BKcRM^# zh9Z8y;_Vvxt?f0_u^3~fV@3Vpp+p7)`0|ZQe9+6X03(bL=;-#zsi_}*^nHmy#&yZ? zV<*~%`mcZQKiaM%aNo=$iUGhOTxr!6UcTqlF$Ks5S!in(htaoX5dtB!9kP*Y%?w50w^>l>8t!JH_1Dy3%W=5d)pmBW zjIsIonLxmw&leR%ZnxFP9(y1h4s|-+wY5!ImbbUFs@feM9vmJX*xA{aWx3nc?!EV9 zDwXIF4V2Q=)eVG@BncZE+ZQfe3kLnVZuorOk&!`4scG78>?Ir=?DzS+xm@APU;g?t z&%8(o{kQ-2UwrIiAD*9|ktE4=-Gjk-fCIcK%i#6OCr?iK{mSCv&em2Q-&Qo=RxyRc zK8$gz)or!9J(&Z9V19noFl@_m-;{PdAR7J_E+iu~Hss4KHG8wr?G@PfSSf{J4mEOG zDD5qF8m}3q4rvy;k$zvdX1X@{UR=HP`h0P3d;d3eQUVx)Ks8Cki`5$Gi4hoOj&1kI z1yPp$(Xc1r$2`}oyK)@180U`xmx2u9o)`nyH(MJeJ%&gj+ubQ>h&gXCO#=`>20$_( zv>0o42t==BRo~THVRrTP%Qr&F#L)a9N@-67(0hF7o$m6vOW+&yLIVIC%Hn>$Kf+zw zR%OBSo6psL`AI$*gJyA5%`XY?u<4JU?1*O%G8y8u7}F39S~t%Cz1dg9S0-yjt3&| zd7tRGZhv3&&?MPiihbs*#c(_ZWzTRz91IG9h`eiJ*Qa_V%ps)Mz=YVenMZ=QMMMEP z1Qxeeo+(v~|a}NF?qV1(lbcQYX07^h0N)pr0A$%DHQMw^lHZ5rhmO2EcPG zt3FY43Fd7Oti2vok`bxiqPK}Jr4-PEs?GHqwE}>iH za=G2+FqQ-XL--1t1_@&x%(qRgy>iZu47WtTQI*e z2zi0))Qxtjo*a)A*IBDv*}A?sHZ#VT8-7^^jKv1hjnX#axOTO^dg020Pd<-*0cFbUpqHF>J57WU;nzgoi!v50ssOTPcZ3&4&QE>yH~5W z;d&CHgLV?W5kEKGRgKu7W2=oqwYapm0RZDUo=^$^oJ^0zgOO6Rx>L?U%H|%p#}n|G zU9GE;frR+cdmo5qh;-a=UJs)N#I%8SZUZwygv6$n3NMj9Z7+_Vx?( zb_3B{+RkBwJ%ZdZv|fVhn`E*$PS_jj3nvzKR$SNNI1U0}IM)2&RIXlr>*qSa0lr-c z0Ef{F*R}xwKm6WfA)kUUeE7s@?`cXYefHAkXP&v)?wZ`UZ_x!I01#~C+q)%ov@c-0 z1igh}Oc@Y3cx$IttZRRL3p7gU&dy$~);MzH&_DSnKlW#T_E);DA3r|-Ge7gOV9>w0 zxwEyk@AG+X-dyT*v_vBM%fI~dzxHdtZJJgxnfMp~;vd_#bKB$!!d$M9&E`i(hd=R& zpAbiuwE~OMAXz7NkY1Nt3GKP8@f_AwQ}!yBVai_5o34r=gh%?Njk2>+Y#tp< z-^hYb{K@_=yffvv^uIk*-fuYXp7Z_1m6pgs27qZhU$}N{Bp&mMB8IToR-ap5A{15C zO4O@tS7SWa1)MVzBBf%ZgZzD|c)roZ90mXo0%iw9Q4|_YeY4bHOiJ~IIcl2~Rp0~$ z0MEh4?~gISTmoIxygGOI z15fsQ6|Pjb3I%;-*9-?dU%0guQhdip2HzctDt!Cp&1^d8Kb?+$aktPRq@j0Gk?{GA z&EZ%yS8wK;O^D&)fq^UAy8%U}i~$H7hdeyvTUszz3$2|$`ZKTE^~7V%t&Q_9Uu)Hy zcN*Jz3I*G-pIzUM$wHq;xMzGk9S(i|>b1cT5Ov2TO!6qMFF>4 zD42-W3ng1OAY{a*j_ER{%3dD>q+Kk*@j=^h)oKX>oSr^JsaD@}T*uJW&ffK#{ikMa z7z#m}>2_aOSuP~rSJj?re-qA%xlEU!^&ZJWj?&a%^d{OrYgvg*@@Nmqgj^Py6>*cB%jC-b! z52S)os0)?-de*qkJRXezg0O`-3;{x%XXYD2@;_+zrK|m9bG+p zdH!(h(8-t+BV{&PP)IFM$P zjt&q0!5@F-`Ijz!V?Oisk2C}!&+(4qFh;@a9eW`mMB=6K%+T`whGrT9-wR6u3NLwO zWw(-}jAD4uwtaxVruiN67{7RJ8$kHu?>pgBL_%pVTIGeS+n;`Vp{<#3(U;*(yE8Py zxv*SHgcL>MzNJ$GV@%+1v8G*JuQC8`GstgQs_+*7j4|ry1n%ol3>q zZMAncLEvk(=CA$Q@BUx^*FS&ku?I&-hj({#(P(I5Vnh_hKlp<`?{qqMJQo;a7-Ln{ zR#!JANqqeA2h-`~-d;Wy3(w9@@jU;Cn;VWs{g!3> z{mRLcQ@s)+#;DcmuC3*G9UEJ&>e+OCAvd zU^-O5K-Ju_xF`ohqtip2Ai9>Nshw7(1{iQ`%hYsBGYD}3fDl2R!;}CEhz}l$oZZxJ zmGpp5a9xHmgb*?c*f&_ZFaQ(*SuVX>v(eKiGRi0=n&n>Sg0{7sAXJNlyXn9VJ4s)QQ z+ufzC24E)~4u@0mR=G+5Oa}N&NL(u!BXO}KMh1rxeF1oDIUkCL9oL|Q24WE<=zFEY z4-rN#af4R%P!wG}S1+p!BaXFjEFuA7V~#@{FJW-cxc4{zc=J=w6=%nTrGn<=L4QpA z{Ax?Yd`s(o{nk=E5d87`#t;gMYG=FES#7ozjQ48w2Pdb#*j9&QQLiKb2+ppoKY8qk zYM9%ViboV22FE;5bI4-V7J1w(SJfL!M3#2eR&?FMJoma<5W<{KmKvSbPN8**vVbg2 zr3X$9;nlC-sujwKvEkhti;|+4mYoU(gNkx?ZC#Nh`Uc|+4x^4~CV~Np=l5!LgkEj9 z-~jU}N=FKO`pTsvA>UNce>PvLbu`U1`@*5NW-RRO`y?qKOOMaa@R%#N)zwl7xekZX zMpKnVyislR$KzumuiEKEMM*7h)eEuy)G$!%9AEhI#q;+Jq(+D5cN)1k4+dkgu|%?_ zbw?91uj1huDd@(4>=8Xax0(|GZP>0y;d492NJ_X|bH`Kg`mO3i;{hJfS{Du{@qWIX z8l7m@+s$GJaVRr0P*~3^zKE(enFhy_A?6R&%QZlW-dTfY>4}*4DxiWGIectcYkO5#;Fgy zd+y=;>MbqglS^1)#0pPL_%eO{E-Gf5I33*C-O?9V_|t>OM$^Y(H^WdyaUoFSZB54#w<-r#A}OCIplhIEXpX>miPNVw?}h z0$=&+n&ptOV=+#`-MXRHbgN~9us;=v%^aWb1|_voqW9n z0CQY-XrTYfwOf{LgE#5%Mvh}WbpPqm;Q`aM8KdD~;PHp=J9qx7?KpS$ZGH3Y9D)!d zXgcI|XCRDXjE|1a>{p9zy(97hWfVcETjuQ0WTjo#EdyhGK!_jUZR{O*-GBD#fzMvv z=!^P3{MdYt?OormeeSu%X2%c(?ryygynTeXx~scXG?3iDndjez*rtP<^SjkE z5Qq9>3L#8aE%dz>%J8N$T(@raB|WdSyRuh5sZG_29v$`60;OO?HxFW`A(#`~Ag zY;rzdt5iMt-p5Ce%m!kSOJ97to-1+!k1z(55(c~?8c&GNEO&Se2?KN&S~ zy;Pa&llo)g$XI}2=nyIi9A#j?)>tZRLOc5I`MLjo;hMr>!*&eYQi&<5DtS?m!1ud-v-w-6mON!%IfH**86@$$KN|Y0u`K(JZ~RwtmdtH=BrW zSF(@HoO$Pw!$B|S^~w)UP2E_&rN{~=`_yh1Qr9br9mf?EpWbl95!oi1rnWLk-%dk4 z9E-l#>>Aw$c6CGZg%T09t`;`-`o{aCgCRfy`Q7dGP#e3ta{JHZlZtr*I2O@qYvT^m+=zW=R%A>cvC!c)CP;JZ9+vQrPVls-O!`{-~ zZgpQDJvuoZs&)`1h6nQNJBHc;6xIu6PsrnsM+5PIYBr3u z96Muxu@QJ_m*ZVgmL1Dsxt*=14k0{r@^qKF-hkw&rd+l7WXMf>*KaKtt&XXii<}d9 z?C7auquH|I9h?X!qHV@SLz1d?uRr|)0ATdUp}9vNEUm5#2er+`MucNqtF85|j!mGX zh`lgf+hHC7-g`3H>S#B2t>k;sKXb7gBmH&ju^iZ3v9c-$9L8l@c3+G7!`qK3cnsh0#< zgo8@|iA-NK$pEt*``Lx_`D!WMm-vAXzwh|bSyAL(K6g!fGygftSTNvsUDERdu`Ju~ z_hO8JVKQ=owS?{*;^&-#ll+j)wi&yr@qEuwd3=78DkvB zeesJ=Klj`@pU)cz_*|FN>y1`Rg%DzlJs$bu#cNMI@q@i63PIp)+xgU|KL6BH&-r{_ zuh(muW~0$k)h>hpA#^ZK4{(6j`y3&t>*h-@-9o6Rgnjk#(9=7JB1b8M5SGi$=byi! zD5B4&*tXMdYnJ5z@b*-cu_XBpuE%#}vtg4+#zzTj<~2^he;-%fw@=5 z8RDScvLXY1+i<_9n?&C3ugi5T+p%m)2*>k|Wg(1%v4}sFIy?~-Bw=&Ag4N2!8~eo4 z9m}Dv3n4@thdB%}hOd}z_U?Ssp)-Bb{S)4^8(L9yL=N^$B?qHoLvvMwa^F}>68CX6 z&F#&&rluo=`o>4bjvtx6_f)6e@&tXu^V8R!dSTk zw92((?|6t1S}zn`%jS5_q3rP)-&)Zsw%y*T5rmp$`DG;;5O%7q>6ExvW7qcD{Sm<@ zV#8s!%Xo|c0JiHA#sGl*VR0-W?$vCE(3Vb54ua((O$NgekEht~B!hv)%`JlX$Bw5O zO}$vtj?HBf@A<*>aGDUx$;#l&ct`E*U%TFFsO55NX}M;oFdp)E9sK35?ET5-cCY4a zpGTHBTvs*Mr9Oo_F&`?Hjbhz8Gv?tjcww#Ul`w!H9TFz`DHX*8RO!lQJ?RKrx@yQZGurCz6xVhyMrLknnv|Fdge9zu$ z%Q1zT#OOFYhwEDRfw9qcx4TrX;MXjDAfr^2eGo!Z?~0;0kxcAVYi+}*b-DBlf#`h`qn|tfvLXsD zAw}J&8P>@}G$@HR%~&iJZO3`!(3DS6^36uGtA#kU-)KC&zHM37KqPu-Ae9Qm4a*)+ zC8Dw@2%HrR6|yxU;v>}p=TC|rpWZ0hylk8H_&}oH=X-E^Hsn<_1{4Qu)oOeoZg!eT zl#pX`)M>j^Q2b#`%eos1=((KJ-{~AaiQ9PF$)4VwTep zAf4+^svVoi0l~0O52xRK;+UqBKY96FBob=q+H;FH|I=SR(+m&^AZ+P7*=m0@-lf>3 z)zz1myX{)NSkjuBFA_`)4f>-Qo(JKKUl2vk1v-VHL|pIy*K(@48g%T;P7k>;kNoflk3I43 z^w^Bop9q_kYcKECbcs2{$Hb=Ixw*SqQ=2=b{bz2Reg4)(uSfa0kN^0C_nrg*3=X6x z$3}K`v+FzCJTKf?#q`b?jfUoDr$kXi2=M~HwXy%~OXu%S+WF>)c!LNb##qp# z*&u{2Au|Kx5P+5JCWpB@_l4!y!-))naHo_Nc;SF$IKV5iKB6cQLGT(z^Nx@5x4UF# zl*ERod%;Y1kKms8@789i((Hcy(#BS-5#VzihYFYPD9Y)iuq)7}>Uc=QzET zCT#!2rP$9B*o!~ zbbhOg&>K3LZzqnvc{zl~$RYp&XmmBq;gO?riJ^Yk=OGSRe(B=i?8Ly#M5|Q0{goWX#Q0tIN$Kf&L5CRbJ7#$x}mI_9~&sW;+ z6Eps$g1ubWn@Gh@kB#P9EsTNTuryVnBV|G8$PnFZ;rCCCXstHJm^cn6;BMC}?&O*1 zkvxGe@;rOV4s|=_K=9AMl&jYDfkeePC&f~L_KXU)nC(kW)Z00vyEO8J)9-*4rp~NK+I)wC^#$V$Je*3z!h}UB| zPQ9Zk0$=Hn2PU~^&TbS4Ju@>omrf5P6J5h{UD_q4A_%?b%-#Cih}UP@cCOhJ@2u@0 z1cX3dnjack+S#Ocx`-ic+s;?^@-yMUJ*ikpH?LNzxn}G1*vLC(XN^wx#{OQ-ab=Dx ztE#zatd+<{#Tki33$4~;qa!ND*Q%ZPkwZx4__n%Kt-;8UcVGr%e0_VfW2!UnN<@50 zz$?WG8<+tq0_s4Tb5X%9NvmJlL- zKVQg|BpHPIQ+lx+9Gvtxbo^MnP*8c>AUrPA;xit zwku1-f+yeoflxf=m?i*V^yuM>xzggwg~3Dqnfdv(i&s@ccL~`n?g0ikjw6J|Vv*xV zW_81a5IBw-jfE0@G1q;~_K7jT^W4i9u7!fZ`|mj}2*T3p+UoV4h%Z!W*WQwK0t9}q zudR2zl1vBz00IUOM3!q0$1@&LzPh=H?rOmxl=x(iPxj{PrGt&f0p8}R9;52+a&dh) zGjoy>mlEer6m-x28!-CJg*6T#fk)&uNtO^|Ebx+L=sVY6GCB=Tl;1Q)`WqGlF)Y-& zg?jhex)Kj7QNJvUm@&{boNC)BH?+EHau{*=JMn`+2#MlrL=X@{LEr#@cmEP&%-t1j z-ut2V%U5T$H)}05?JD-u%prl&r@0S@pw zzVgl3Rrn?y-?u06w|O&rKs0=pdvfZF%2+_nl7@x97m?izU>+GQ%Z=O(V&&%7Uo33ybepa1-CLUO zOw31OQNgB$Zj+dg%QkF*gWt%t#c^3W$RFufzId}enG`X4B~b*itR%2u1+lsp-tR+M}1@_aBa6! z=~m}63B$4W>Q+$UQhv@ai+gn&0MNDQyJo%11=DmWLTn%^woS59G({dS=Zll6z7xZP zs$qKsbY#ePW3}Rl-uQgt+`e1S75&k$V_AsfA){8i&5HuzaCW~uH|%-WiSdewRReGc zW@3KFCC#o0As~cJkNH3TLnDKI%D?*feF1a{waOF#&{8ueWU&gp!_|LNjSB z=&?H8o5eycoBhXwgIne52TmT#)f)?Y`-Cy+71;t~3?m%#`_Hbe^}=l!002Tp2`|dt zK%~#Ze|9O4;q7H21Av1NLU^sx=<~=k;lL9Esl`U?xuxaD$41lf_zch8*x6ML1M$4X z^NPrEnC{eSw{~^`fk$U2Z(O@_{OBP{8D?-MBy%1T8a&=)e96N@2&R*RJm#3|`Z)-K z!H)%lTemKrTWJl1BZ?s8>z2)s-{)`F3iXz{v}vE3&pda&Iy)EDE7=W)&ZWm2dUdKl zU9D)Y)%E#&ARKMf>PIHx+q>Da)}Eaji%C*{B$}_apTD)SQ>ffC*N-Jc8FMTeiTgJ5 z-N~tN7FmICq{A$f%lF}ASLi4~pOziN^;oU3k9o)h$mb#7zzl9-5Zosg3KmAVT+j=-t6gKQx-#n}xCA z*wHhiU;WDq?eY?b(O_R95sln;(%T=yUwyf~S>au`!yqth3L*3h-e5F5`-EqHCVXSj z*xG8c4z-!vGTPO8(-V{@&y4b|P^YR_a&60Uk|RoQhrk%1E};})9x5?mtUnVCga80* z*=^IYp#>XzE!!jz0?~8%u^)REM1WBU0dq_{nn?x%0p4V~W4ba&bXVYc0rS0Qkwt6- zpr+}9BrwW)yeI7u0dLr5sGZJV{MA=pzHmhlg?ynzY;tPqaL3Rr*Fpe&bHxD!&?hOS zM#ZmqUFsqTAObxD%S0%8ZF7kMfYI$a7Xm;j4SD^dz!w`8MU?2l-s1potLHZZVDrM$ zX15(1oKiv&LGitA%`PF>+w|UD3t*I*-BvTVlUu%A&uwsm^lh;Wx0mT00sxjViiJQo33pVIVd#q@bw>xf_6zuPK0Hj;1~qrf1po$gpj4_lc$c4 z9i1nxYjm_wJT`sL$*WI2*Q(S!K94AKxyCYt!&itm)VS*PnSQKD7J&@0zZv zfsE8D+`8Fmsi@~h0U?6Gb^*`Xw##)a(Q!JtMk^BzZ=K1ScE}9t|l84fh6^F~u*cYl*!*viu zgfWrlY(g$>Zu(_86N!w(V}Ek){C1^!Vt6Rx4fTg@+l7C2Vfo1;bEa+X6s!G03?f7s zVHh_$>Tn`13j({#yo*uBOCsiYquby)&M!-=g%U!jw_@6@SraMc<&|o9pzmF$PkiCZ zm92Us6Aq;U!4IE2u~V%s>}7km8DVT9mF!xkY8V*3!X{A4a{?8W!^!Bay{t`M6NknC zV2t?%zHVCQinV^Pd`~h8Q}Gw}3+J-AsZ{cP$Btaz-CxLN`#lO^%q4{9`B>jzQ{ojv ztF{`K@&(tjwQS+Y*jO#Q;PLN=r_cQ0BahAcgrP{>Ap}Cm0HCg`NaEuUKe`=Fi0!uQ zP@6dsB`_F`8bqDKqh3EhIT+|#2?2|zM(xdt+ZPMBwyGpREYj^5c5`P>jHeSZWpjVe z#ld^~qP}oAB*Aj2xmj%A%xwc!HOM#N!icc-**@;&d_S-7lJ^u+8`A-CDwTyJDc zSQN}oXW{IX`%WKnEjIV)gOeu@8(pouv)i;AaiR0f zX>@+)&o(XWpFBLiePJP;$oLfnKnS9AWAM*ub5w+H!cTDUUz!;Y*weweQJn_i=E^!e;`D}S_x5&Mj zWmK=7duw-}Q6>mHWo#qAJDC~2vT;)qq;ENKAP9SDuY?c?k?ptvk8d(PvbeYIQkTQ8 zPPmjYffxJ2iH*XZ!0`ta&I9~4_+%qOoZag$mN#$lvIlegTinOHwrOe|Tkmj!_??Z+ z-eaD_kjLEJI!DF;1MFb5AK(B7I3ODS-m(msN0L|QHcia^{b?j%4DbTiE^5iikS8qa zO$+g_Mrj}p8f~ZBFhhOb;#z~p+&|D(SO^Fq!9_fL4P>}oyL!|R3;yCcwq5JE@^rIbJj5Qjxkjt^!MVG9%;$cPy$d=ti7;zj| zD-^?hiDs#?b?xT-!}k@|xB6zLoUNOyFSYa0!9D}T?>`=D8}#z7c4N;#5Tc&7H3YWn z>^2&Tgiei){l_oX;yx%!LZ#8tT%){j?d3|oNo8hqy4&l`ws~~eI}sOdWX%!*dJ!50_wx+43=;3sea2&r|F2DQaDX%DX3?1LS;fD|aKwLK#4g&xi-7b11 zgsjkZCQ=fG$gVfzQ&SDoB9v}ds)a^twN#u;roQj=iS2S_IbZNflHzizBi07Q;+2rZjdplkRA{!}V@xm>%ly|Z6$oEjO51!g;~hUv6@ zUT-*>XxHnqN7yfxe(bTw)@)rgEJs!*)9FgUhZtc*lSL`$^I`<=wD}=`nvSgx1m@z$ zYFAfqx$fsvwiu8&)DQ&M&q|iNtCGxAWcg;-6Yzzn{0$=XPmC6ck(lw|rUMX;1cHb9 z!pz|kL0_ZWsTyb`qR5`%OXk-6nG-2LUutT;L<%D8L$og(jC0(^%04GZBPWszPc5fL z2KTzPne?dWm#sszvfc?MMQ>CB0748QK$6I5j5YFIVuRrmnd(;Co0L17?aKDL3HoIYk@HRMk@BP{Jm8-X|0ZWTW zp=d0Y-`kcv9*(g5UM@2~KmF)^oqE$YtX8F3-rkMO3)NynHpNf;@L(#!0|4$h7Ft@< ze&hE@Dl{7Lc>-L-f9%Bm?uIK`zOXP72AfNz=U=WM1O)*B2xUR?hNZ}WPw&`;)t1pV zm4NTe2j4OG@crIU&}p~UufD{s-f|scH66L_?F+>h7Sv(qYOeQiQ?^_87nob(O_gUGq#c67Vh$Nc;gFYQL>1O5C#wsN(rU1 zD9sN|Z58)ay(@69X@h5g1r#sGakX|s{KkhpzyZFSg%LyuB4l)0db|Es#~1_bSY>eeGs;eW&Jf z;^3hvPr!fkg>(K$=+PhjXl`vQKF~*qyL9dnVAQbL(NVd&>_W{&fH8~^#*KV2aqnr_ z=iR<`Yy9}(P%7En-TufEv%mY7H(l`T;GxM#U&0p-&OLlzr{2h|ZkW31$F_pNt!xY9 zU_21qDOYo?=1?T+6?Mb57(QYUk0CsE5$&@&q@f;sezI>~_UA1`(U1EiOiq_FO#fr^n zq%Wy=x@x0ET(_<|$(W=W1ONsB=yqMgKt#dG0C#!UxPQX?g=?+j_Zu9CFoFO8kIY@q zsrgPvcL-(RZmUhhae9*Mooa0{m!IzIJ3i7s5=FoJf}z=*PZHNlWsfA?H#L5Ce?8Xk zHyRd3sL@d+9`pFM`X&ew!i-VEXw2)=ZJV-JZFndJfB}lYm@GFa)ga&?6a;}X*4B;7 z+uJwy_hU)L7c4u&Epw)IJ1-Vr&a1mB1+o3>wDi&?5Y`MI&wQ2FP zk0G3E)75ezS1*N;RH$$(x$0q$ua+-a3`7BklR+uO%fY14ZN&y7dfSDVW%c@WYE+q1 z1ingb7)tp=sVIUOfVgPH3%h!l7m4FmH;vdxL=N$i%ZoM=B-6EQe;d@TR9KrAeO4GhfBD|#y@)02e0+!~i)prrctghyfxy;NlL+~1Qx6^2G7*4UJieyt`ptxYqD^cuE+tsW&Qx(>`9r`?sow>Tm*9f_!Fb;N*KA4gZ;b16BA>{<`WYm z`C{I7Z9x$9cE`0HPtb2^`fGysFs{`bpZs5+gAllsCIXRkEcwk`^j>usfee71;{MFw zc(omD>umuG^bNZ&2q0lJ9*ox227(Yl&*1^~oy@Y`HDGX}m(mf-*gIKTnEM{$rk@!j=J@Hm+A)OJ8ec}cDG;;VdPi@VdRg=-KNPX_=gxk!5HMYPOX6j!5;|$2n*|*=l|kMjY1LP zUd$D*Mvu<>qmfRdm0R6dzj(F0UkLF;N~HUyC(TZG{mOMuDEQug`cvx{ zuX%!ge>|dA8%r;qcMMYrhYt@(tLr5e=_6VfBGhR#0RYkdbh}cs43k*a^urG(o2#2u zhf5CCbGhn%9zcjW%qVpocVK!F@xqxwu0_R0U0vR<4JYFcqix*)03?EemZn=SeQd`6 zqxXjW5_Sl)To&scTx-8}!RwuV#>;0GpZ)1^rym zFD`9#{>$&K7pmsNQ~qMx(MZMQu|W?*pt5QHKfk?R?{vE+Yii_9s0W15 zB{bsodL^l)>j0p|T=v+Y9PkdAE?O_-82}zp%r+YRkx;_p*L=2G&PzI*70+dn}_v37zii+i%o6hWRSdzyOEnOgh4I{dt0Xu2+kFk{SdiRsv;mAh>j20^ei9^G&WYVW796|svgeuzJ`p({^OL@1}K)h@r*Dz~I(OYO$ zflrF0q7X$#V*nvsbG!h9P(-O#8W|q99CY9OXu=<0uDzBkRhvyk6jZ>Cr~9riTyLwk zZka=ap=_bqZFPh3Sff<2)y_yLuvhL7KVy!&dj2Msocv0*TW_hYW?`>15KCG}tK}P3 z*NF~f)M}x;UyA#sN=-El8$!tOyw)k=2La1^ghi0hAe^cG1^jY=K`Q8s&c z@`#5c#a6LfOJ?F07fblPP(~Zu*-EL7Ax(`B1qU-j6N3Xs4$VAx$`kNU-FMR8m&m^O z()OiA4;1^N3Ep9((eVaise$yvr#zV$HEhUXNC*pf@Na!;QG+-(;7HL4P2jH1pm3a?=};A{k#O<>O>JdFI6M{88$-007sr1IZXEtEAT6 zsphiP0*5h#5Z+~=fFLw&;#2qsPDNc8JbS)uxkPY)x|1vJX9d5-uYr`xVz&z~Mt>}=CxXkK0|1g(#5Fu03 zaw{AA%WE4~uHSm@YCvkQLMF=_dBtHeqHTgUT9Zp z2xGM=PuTMx~D%p={vijAp~ko`_e+= z%2KPo>RfxF{Rc1Y%nf)dZKr)#dMJd@cHM~28}WH>?ClLk<0ps3D?5c^i`+9ltcYS$ z(^SKd1!2G5I5srs3o5&X8g)V1qj(grtSD>6QqL=aF@`ZGD!#5-kNN`%zi+Etap-L) z1%!a(0tm@Ti>hJ<_8BFU2QUz$VB4} zhZc(aM}`IvfR{ElF~*kTNIX9ni_8xUPG|bYlZjL)G#mfA{L<9$cwaQUxxF*u@#HZcP9{PO>^ABg!kKV5->73znM(JM zc!ig@cMAsfW7TEGS1vF9{tIVkMx(?M-Im=jpr)ISVaFw@Xw(d9x;8M{&3Hr%MU_ei zs9if1jZvPAr-!B!DHrOz$X{OCVStIekS~`92Qq>uaC3W|XTT%yfUpL$c!WH`u%YYJ zcEO&h>KOjq|Ei}<4oL`ZL09ES``tCpJ+ll^;Jd#y?*6z|j3d~rLU4*P0tjRJr$ zX1z>@QYW7FdAvfkuIWwPv31Hio0m5o)0}r+03t4 z&2~Lo_YV!1w%1~r)PvvuK`8NpSMdY_6Q_?VytMq8ui2&MP&(Ds9f%;pNO#P&<<)w1 zZ+K81NJx^5U6%|UKY#J!d0p#@y!bk$HH5%0>2SYt&++K` zPUq@kLz0BPaGb+@*EMo``>yHq7);wR5kj_M29r@rnc3Cgn?*S=#ypbZk>yge_Ev}n z0058qme!61BLR=ErZ(OW(D)^YyYMN)j*n6_3F4i+k&DWEmiYj_V#Bnb|Dv z>z0WQh=v0k5Df=7zyS_$Ks5aQ_%g$3kJhwo-Tix6(?=M&7K!xx)r#@@&&L2nP-~cp z|37>G8D>d#T?eAagpau1-@NI4W@VP|s`eg01I>mZKm&l3I3z`ix@Db;n`H|I)0?;5Eh7i@r^`pM} z^1Cs57H6!{?T7Y?M}3 zYrFfcawYJ5UKA-IS}Zzt@|YA@H?OYh?Jh40_x#8cuI;RyzkKYG`v@Vv<8EBN(JGcX zHFRP++b&gZ7Tx}lLQmI4N$gZ>nBxM^&kh#K+ogO`?&rH;_TKetD~;W~bbp^9OJ+y+ zEXQbf7$HhDd_3vDaHBaikT6}p)^Sopg-m}z#ay+~Xq)!2{={Sk{prPKe_T2}tWX9d z4x1kRw=XoRdXNrd-KMrrhmzu0nsZ5Fr_q@$B!ZxQWmk86aB4VH>p21O-ZvL%m~?Hg zTkm+GR4ODRo-qQsdQ(@DiJ@$!va_F0CRR#i2!TW3(KHD2ZO_Io;tn4|A45!43?i((}U)RwlRNLJ@xSc4avLNK5k?lqU zL8wV$R8{}t@>L3PG?to(hvp0UxT-A`OVfqIsga?%fx=)S!68n!%%)*b{m2=l1%g?EDN})C<`~2#*#_f z_uHl!l4aX_}NGNXf%u7qxZ(KV!Tqv|kI-w{nD${qLmJ<=m0D_PNLE!s#&s=!v_5E^5mgt3* zl}5K!*W0FJdKAba_x`(+ga8CVNab9Hzk1ERzNc00=|R^31kl>qL^0$^396lWJ8f;Y2*ymxll#o(mk056c*Hy?V6!i0 z6V>fq{B}G7V@#73MN+Eom}p>tVT9{?Ya&17_`YGAx4Th72ni)IEh39jtqTvvSIK&|iiH3KBJ40$XTHn^+#Rurx zBsm!B)~#mf<;s^Bf`yWa57he3#=O*qr5l%$A z^`sY#OPK~B0;xeNhxm&#md^kv@4OqrAAJTc*Py zs>NcG#1rCYG?a}9m$vPDCPLTuY{R7-f(Sq#(4RjxR&wq0)mA1X#1wA7?fc|wx(NqF zLpBoTc?m#$Jgr>b^*Xkd&@@rt>pgv?RGI3_4JVT|z1uL%mSNm~Y-(@278SY8mVWot z)az?&Z+Y)wUW5>Ot~Hs(4|VP*{(;laFS=Cc)Z^8+Qe^z(9PYQXeja6cy_N-t5fNY znYk)nA9iSe1?_~5kFZkIMTh6gj%T9-nIF-908gnW<4Dkn*3;q{%=po#_NnD+k7{o{|` zGj-Q#&vD>^lQF_TmQbmcg_>vCeSK*hAj|S#NXyO+hLZ^v1Q0@q5MxYK<-+__e59X~ z#ZWlHONt^%9M5AO&y9!gIi3Ff&+lE{_ZwaIcYf`%W7AxrU(Ssb!tG>;ySCumytz4f zd{RlX>||J!d8bDjyS8srK&5azpX?iqrc#b=mbbQyW*c{1)kCIbwA$^~%F6YxK5N>3 zyIfJip?EG!Jl}U+76g<*%Fw;1lJO|_{2TRdmyYIh81Z$zX*m``=wLx}dPcIp;M%sN zX`Naf3w+=O;Y5rwX7>#AO;o}`M}s2D)y_L58V*z#D5H(8K3y2DbQ-!UZ z!@6mdn$_V~h?p=F}k$iH1WQ;t+>8BpSfG zWOO24(Aq`g-K|7GDT`#a?%R!rKnU!fpBf7F8qN=n9wdCzS`xnbf56_xVhSPXQ*i%8 z=+drZx+JMU#AvZ)LmDtFOb&$4%%m??F&9hbhYHbjS__95gjToPu6J@_s6--7U#xF# z?k%sj>g~ZOZuY#6O(l_M4EV06#iK*>vmD1QpT7)TTT_IVZcpBMdiL%!Pk-t&$wF@6 z$dvDTW~XZ##@6*k3UPl*&cwu*uGLft$NTbTM_0mXvslI)hcMPs=>l$Jj4p1PzR{h( z|L%=zilmh?o9UP|6hp6EEqMW%o$hP3tmVb6a4a0l=ZtPIXy_wCqs2!Z zEjQMmEgR0W>;CC}{?jj(KmBqIBZx5WI<#p8PaKarK6_)edt|V$)M^|X2(QIPg<>fmkIohPlA6{qjB}eiiW&+mLs7JdBxd7Ut%FSUd28-6<(^_S2h-z$IbU=Z)_C-lt)tHu36vN ztKKs<$b~}N2DRE%*K&qZsv)S%a(cD}A(GUHD6*)+rBdly)7snJ!H5MeRTFY^yBkg` zazyZKLVQXC8Za{Xa5kKQTAFq{dIXAX%W+H}BETpEj1tNOh;n_=X4|Z;v<8l)MNKHJ z*GA{&Mvff`yx`Wz0AmmVAs(esxoCFP{t%0(Kv5%o1v!L6$s{A>fO+J2-stF7r{`KW zud2h-<8m?)O2ni2!h`n>&K!wXAOHD`v!%+&0Q*Ys3E7&w-W zjcd8#fwk94onpXoS~!tXBXN%7+LdZ;d#AFyXLh=AIW(Baht<&Kg)7%zJ$LoFSFPRc zUafxa#dDEZ6k}Z9-A@k`;(Zz031p7kbu`qogL7AVvLNRZ$+ppL_S%dxh`w%NCzNQ> zh$zcuyG;W^NI(L=Z)~KpwaW?oH<&9i##C8RM5)?ozH_4CmQ;lAAqXc6Bm0dC`g($| zKbfz0n*zt5nL4&pEAH0IBKM765)eW!@J6x&7<22D9UdRD42L)*8V+%YLmc9eXn3~( zaWIsU^@{cGCR2z{nkmfLOBwi98Tb95=RmvL#exGVpr?Djg`RY*7i!SVG@Ka1%uO%9eyLTiN+Nf9GG6l4(oR{Fq;M)>baX8mY3!9a zfe#2B$S6}O_`;12K+w{=L-Vs7&lguWAcR)W7&$uIthG)RxVp*KYYwn`if{0VG#5>G ztF_&m%Umq}=!u*xtIuBEU%hfIrpT#6Upk{sWw>Qi3*;!q&^Aol_9il-ETDywn~ZSd z*?g_r$VJ4CEk)$s15>fr){SRwHY6TR<}$7yh`cX|itPjt1bW+Gyp$d47ZrK!+QRX% z(bZD*#85UW>6dq%Qrnk!bYQrE5bSo0nG|$*txNc!C|cR?nm*(apbSVDe{5{v$(>T& zb_oUJ8OaOCe#_(W*JtJQxn|wL4p#uEcZjHtQ2cnJB4pIO2aX?~&ZTDxg_EOWtwt#m4ri0;cqCG*78hH+sOXrrU~D9@ z(P)NaAw`sp&3et~9+~J*sG%!cB^N@*+2g~7PRDX78XX#3-`SW^$a_mjm;fR4i$p+p0c^xGuHN^*w8zc ztfUDqJo26aE+Snk;Uo7?X+x1@F6Zho>O~T{oE%aCfW3CR*KGHiZM$dmj9x;E3?}o* zaCEm`S}SiOj4{Vi#^O;8BG~TQ-Fl4|#nfP7Z)v&J(9hgGefF+_ax=Jcv6TvIq9~PG zHOKc5MBluq`;ME)XN`6z(U))RmpG1NjA`*GCDiE|5TkD*8dOF2wuy!VTUOobMb&UJ zlBl-pJjY>(d>Z8AX`bV3&za~ODK^Rop=!Iqf3ssNqmm#MlDSf=YS`AdyZ1vJ5)Fqq z#32rGh(n^`Z>TE+rA$jm2qB~Gz<2fS!YJbftVG3b!^UstWa&91lvE%9w(h;l+&A8i zW$=@Ixw9XB?9^kA49rg2hNagVj8Xt0C4>Y4@dFYN0HLBPQX=`tOsFjOMKYN{P{vbS z*F!$VMmk&;~c{*uC5g~c2g2T99Qm9fr9`5m$31S*tSWn zWhFAnp}83d;nuaqUb8JoB4tpi^*NDUO$FI{sS zHzMPsW6A4H-l&#ERaIkAyW7)Z(fVGIo<#an%0u>P(Di->#XlZ-7?3c zt91@yJ`|5cbJ@|MQeCS$nuIC4^>{J{qAGu^AU<`m zHa8sB!pdTqVB#W(bkG0LNWln#*D4JOBPU>^DZvd`xf2}7XT0SE0RTKRBFkd1-g0i_5+k>f1ay}Gk&*w&FuT9V~NEEZFgVyop- z%AteGHB}=CUR3N})AyW%X83~@j1rcKMWTh&=FRo3uGRDXyG9BzgeooF127*A>$a_k zVy)Z7m~$ZST3u0)!Vzsc89KMLM5zEYq3d$J($>uQ)cek!iSRs+Aq60>i6{FDrE2YZ zsZwq#TOG&*h~nr%CKb<;Xg$R|VU&6V8*YgRL=&>_C(yYX0D zO(yzgCQiQhp|P1UIi2e4HM-4OvC$y5gL&lJR8&P?6cFYFRfIt5pUqC(n-yftC?Emh zBrNbkuPLWSa%w0VO2o8yOo@bJiLd}spki6pV%cPCzh|`D>EUcD-7m-@=6S?%jFP2S z&ac1l5`^gL%jct#&}%oF&F}`RtQZ`_*uH+VytWz3W&~NZv*l2ftO#=hgZz$c#LQbGcs#KST~ux;3aAn5g`Yntha>7kq; z=o@Q0l)%iy+^FH$TU#3#qPNvtfB*tU1w{f3V2mlHwqg3ND~GgjGEv_xa-wj1Qv(Am zB&nh()jLi6ZKnDVLdHO|+wM>0AVAel!zaXYtiEJMk>t1*-LCFAo)=a^rDpZuk@>pe z3rh2eOh{Ii_BJFzJan}@#39jeh(jFW5P#E%hQn^d@6i2`QKm#ByXOJQ0D|x0LnR({ zdS1Lg#25qLL^B`^JuB$etw>(&HJx{Hw0H32sL}9!Klb72yU%a}&nQdu<)8Y!Kd^c| z*R;dQm==vns>&D@6nS83Tu!C}vf*64(5Q8rZDW#%JDc6Pq0pY&mpXQ)50ReUwR9u! zJiXp@9h>8LUf={(;dssuC}Zp(@&^ns%4iS_A3c&tg!k7r^=bnm$V(ysEb#qEK6dQ= zk8ZAR-FW%+fvE{z5=B+sU0fZSpQ-N^^?Czgd~8r%X%cFi03ys`N`eDLdI*3=*qITj zZ3J}?7tpQUR=0MzNADjzGP(T5B}vnId z{>7DM@!aLHzHARLUQ-87&Yn*Bo&EK_g>B1YN+cACNAEtOUEB}CgQIuO9%*##Knsn@ zW;#UchJSw3uzbuxMj2(472EA21EGWxSNN{&`4j*`sHn+;$nP#J=Y|J2JH3w`%`Fx? z7{^7VCM~QK9(bvLZIUO$UmX5?Dc~$<*>h?ho3VXZ269V8< zc5+A|)V;Q6rNio0z2f+OHWJ}E?#lMgW5?z%Y;0v?vDI=};`xBm7nhe$qR|iCG4tYU zHzTShaU4c46$&}NCrX;gKzV*$mYs+g*+w58nhoB;>sdOF$4u;Tny%WQOJB^0zx(EFcgi^{tTvbCDe(BnE z!14X@cqSbB=pA>I^v>nYEjJ)qJRK0<(W`(GfH0w76JH4k;RP{(oNpScATi8)l47-t zCyq?5)|<~Q-Yg`NkIv3c<#W$1ulh;?t2nHv`BXZks^giAnjh&{o%~Q}aQNQGX2-)K zPZ$FXU!ctAWs02yPi?$nL)dVBcTyhPQdXTIc% zYO;S+4#};`?&YUnQ6PhW@evZ*gtCQ)@zu5P$%|uW<(m1b$$&yNSMR zW3QMQDwrLe_<>$+C}W{;Dq(fInB%@_ooCFz`I?C0&&RFJuMl;vZ`LQI3C>|)G-El3AgqPH6|X|ymJD2w?=(52;#Z?;M@$Q z)b92mLiw@b_y59Al~&f*FJE^|TW>Tgn>%ti^h3Y&GksH|)F&<&9o+KkPruSPIus=E zFP@bz*SN)ns^4Aq1LE0s;CTmfl9H+d#%`%%ymawo=^po$ZBOfPPC)-GLhOq&-4 z1^{4;kU)uqPd;+5ZQ2*U{7f{JR3jltQ!vLBSJ%c)&aYj(7Wjdx@iCR#ad_9#0fdO- zD4~D>1c4W@iM)^u;Wu`z%rKw~FaR*eIi|IFZEN4Vg8kF;6RTGi zMIIvvXh5JKNEnWZo&WO1rjQ+&dhkwXrzEvnE1P?&Rd1ky?9k}EFtoL@S1r{~itX3V zS67-K5>cv%Lp(fje6mRk*L-|@h(F5A3(GALIh4|{iaCtei$&L$rgNFS7PkE^Wq{*2 zEgD_Fd>zZm!)FRoD0=bI?veh!>$_WFj%SP^2uz>N<|W9$e#@K8iatQ4x*gF1pBK~p z`G>Q~#r<;CbYij?P%85fL1=pny*XR}A@BlLhzo-W;g8Q%@15>L2tojTLQKz%$a1r1 za0q!p;0HlOmJHY95Yi;+{K{rNBAl8Ue(L(h{9vDmxyfAa{KiH&uJ*d^5YIt`dXA0X zocSk|o*W)tD3;o$$zi1OI3U8KN5)PJ3oGUFrHze-?=?lfY#JP65k&?NDv?C5Qx7s> zj}XRKHX1p%zG1ko%<~u_h;W$a2E%I2brnTpj1^n$mT5H&dmtJ6JC8i#AYrdoCXPi| zM&bok5)bmdgX3n5jSdvLj&bCn2b=)&7>*{m{|* zFYnZi9vB)LODl?N)dEp5UEeZHh65B%BVM7D!Z&RIAb@RO$9=SqiSD{x*Nc^QJ0bA> za;PB{DyC zV{yB(R#d~#XgPrsFbG;@pvFZ@r%GJdzF6J9AkIEgzyiaV>ohE(DR3UB*6Jx3kLP2a z4H<%xh=RbaZgeyKT5_;|`s8~Y*KsPk;ZPdWx2|6>+ioZ&#j`1y7sKhqsrNlJR2Wn8 z-rCh&8U%e)6X7K9I$p2U4m{s?T;KIKu3Vcqnh58jvXnY{G_&DFl)=6sVcT(<6gaiw zy1&$r%2$KcPaC7QVY=P+{0K)~^xBuVxI{QJKeBY; zs@HDWovx%Qg~_q(@L+L$%hJ0f2sn-d5FF4An8T*7CkojE<(Wu2(Jt4ztyVhd4<%x~ zRvTlEv4d`g19cik38REj^p;f30f+gH2I)A2TWwt$6+ijV2PIJe08vW*^wqDFS~Zau zIK+9L_sw1sl+k=VV>outHbq`IBpMEJh(jFW5QjJ<8s2@44e$P=L14^=F=|djW-v0tgX8ym%|wW0c-5MtC6k;Jcov%7x*9 z+*thOmlt>U3|WA@cq_1GjB$c6cmLh7RP0Ot>2t2-cNEMSrrH&;xG*egdiYH65w1xdU;8AF(ZL5hg9MP5F_GwdpB1`kIkm~`}UXD z?--4}wq{gXD>G-0XNL#%{XL8oj{qLB$L1o}i%zxU@ZFVerCgXApB^3VUcRA%?#1gX z$44^FeOpZAKYnJ=)0-=HPgc8~raqDqADND<@75QJ{Oju*%87AF5@x2dRE+o(0oSfJ zIx=Us+J2qVxzWMh@?NP?%8li#JA01L?jPr!YHb4*GJQFSki>BZXOA!-^Kc|BE|(nu zzFIP+cYVFVFd#KIyOC+JwG@ns(?-m$}cP%J_QII zSc^c{_gkhF*R-~2u9Pcdnap~%nu$d8as?vs@LP!qB+nr`qfTAFUW%rl;{2SHXWH{C*<6Z}@I!I`45P)-%d~WSV#c=+`kNq$L z%nb+xK;lu>h8s`4_Ug^+Bbk}5*NzEH2GOlrD->&$_j%uFs|1snQW5>~YBQ6NtH3jN zS6CsoR&PR&I+maA&mvLs10P`w0boJkBRuo)X)z%-c6XBn@Lg9Gd=Ebd*io+bhFi(l*1{!-CjQ9{i&-OVHX9q)t z@o1x{FTAvM-^uvJ?qHS0kmwhVB#lmtdE$L1y}Z(U>U?Q`Q89F9O`|Gut)VV!Mn$eAiP~M#lEiIs1vB&@qtG-uiYrGZQF*SH(JqjdT4gK+iW*W zm2RU27(g6mltKvYZnslzMUv_DON+Dj+@aSxz$3No{mkf~64rdj#TA%W#N89O4j%IK&~*0N!1de((b`>BlgSjHaC&il{Nk>3ZK_KfxHl z0yY|UEU$)=a;I#*J5LM)nBy9I#hr!anY-@bG#&s@T3z2>STfq31JC(`9A4HGj5&|G z5FrGZ?UScJ{n^2wRKpzbcn$#{LWJ;Jq4wKPFO$G?+`-w=L@r&bw09Pk1J6eoL3GRG z4MIp6n>>9yGdytPxo6FGH=K+|GYPZXOBMPqfAxitqeooJs_&Pgs;G%zsY(+$(XlLu zAm+G(MhVK`eG}SV%Wqj!kv-q@v}mMNu5wtwn6pi*wzHp|noP5HqvKZeKoq#mYm4J| zoRlyN{5E0iq3Q5;!&|F30*3$qR?oQk+WGeI&|N2|LR|ZWOMBg3C&ER#SXKakE*!xUn}gG$Zpw=Gelvd1Z6$)Nqm$xZNI5Vi6^oOlX`FFjM62I6hC* zMC8ci%7q*E9?xe~@aGq++0x~sGZU8YPi8VV_V&W6>IUTWuu2(Rt$O#2s~dIK@afTH zBqoXq#=m-e`qy8*%q%yQPVlnWthY5DVuW-nz!*XX2m=_wM`l9HWv62ik;6U5RRuxh zIKmi)kVgnXxL&D#`1GmE+uO;I7SlA{vUmTSGP+aIvIzMXp*R#)^@v-TwXi# z@O_DZa-zm#==D0;csvyj-!(pdY-pg^YK>>JJjPAa#Do||_b>ML+NK>4a**;dN)f_J zG=UMaIt@Zdz*sgCIXXBvmCt7)k)G`=m5LrA{jvDq$RKuuuHzu&#}$r8wi5Hiwvb^-{r(RFQ;w=~N zRQ9??uT!iJ9L@JlPM+-8u^jzs!~LL5d{ zpD#I9@IxOQYTKl^UFZ2)yT?HmVu5`CK0(30=MhD3iw>v7r^jVJG;u0UjG$SqZkIaQ zaCB}ma_(};ZdHwq;rD&)!_&j=Lb*r=;}PaG_e=XrE3s@w4y)PW!Qr{t^7^LH>9#8s z*LEm_`Aj?&j~K3ZYE-@Q^7_@KBBP!Xi6rtl>ci%KZQ}GDiELkef8Td)v(sF7<)xt` zQ#1F@ck9)5t>G9wQIR2pBp{+1#0ONLL;>nf+&Nm^-ets(_el(-484Om4gi1=q>P0^ zvLbPD6c#xA)~MI5v7}r5EZ2dj7ARe{T-q_kWhu+M(FT%zEp=e#32rGNHiScTh?JX0a3yzWj_E1 z3lN8lmXjWd$RWXMyNH9|*wa86C*kUbK72CWZP<*kcjZM#2w8gXg+KfgquGx4*z{Y3mlIVrQZ7b0N0Jl;*$%#d&aa(MA)i3;4dlkLFjv) z912gI9*JmDadm5NyLj}D?Cmae5JJy(Mvl*o96NITwexgqN06l0Kwqcc9GaUhZEQP+ z70+j#Zm-qqJ#;ix?)Y7oPCjE%C^6T+en6d?a=C#FRXOD_C|Fq!PKD)eY^BBIBUx>1}y}F$U+V`EA zfSKXcfLC3p-#3~4(<{Yty)m8Rj?U+|O;M*PpC6W4kc~&A^`O%71dfYDLNyx$)0Q9$ z^{i`K+bzRK7~8pM(4RX#H&+jW;WR&=O}(^33v;QcBxLfrs^^^;=->7nziWkf7Uk(H zH=8O(gE9QYYx<9m5B~J@XjBvs1dAJX2OK{J=3Eh2@}c)W%o^n z>PE0q@dOMRLycZHp37*G)U|Ef^Fp$uNs{aPUE2)Fvf+9WMd_Fp1OQ=Rcm4Q4I2zYp zUs)G;{x?4Sp`Us5(NDd0o@Xp9%lqv%j}QX@AvBfGZ`A7dOio~gw(IpP+dCXa7^C&7 zd+$V_B=*&Nz1cz^rIfF?j^258V_}uqU4*e4cvrW#_gif*@SnPN17l1W&8bRM6!uJ; zGQeZRV{|Zd!>b`V6!mP=>9#qHIgBL$I+pbpSFXl2Wippbg+tSQr+i8mw|Bgdc5QVn zD$6lVxVp99mkfE1;{^mEdLUzgAiPp*?zh|j`2OsfRG5bl(tz-ILJI%pqYwST3olhI ztJ&y$^!{VlDy{$Y)N{R~6Gx`|-dHLB=PTP>jAw?;unbPNKZDTP66uPrL!UaQ6Vs+v?C*jrw^IXO7t*pBCdST@A@FgiF; z->K~`tPY)uDKRA;iuyG05KZ^zB~{@-OxEI|xVV3*DEVfm+I9>*hLUPnro^MvXH<~F z!sN+5MTFZoTa`T{HXM2IOsd*&7jL#A3@b?3+20Fk{1Z9cajA;6fX?*tgJ~eo3m;p`>$CHIzx7AAIGsF*k$1SaG5}#y8 zhf@6mss6rJx!gB9J$c6*hq(_ueDCvr`lYo_vu`HO2^@rskhbGB9S`s|W9Zb((1~ni zYX_lW0AXXVyngjUs*oR;9f1(-FRyv7D=M;Mb{Weg^6B#C=E(8+u#wcO_44L^vM=8* zw*%i{j4{ecK>3KoL|EJH@R+0IV3dL}3K(FF{uSvJ1b`R0rI+{8cMcx8XF4E+F@O-< zsqGn#$zzVgob9^^p|{voKkzfrIFI>qs|Mhqs^1~L&uxt~@EuACyG`BZFaHLD7;zlJ zJOGDW!yyiFh(n^`5bxw&Lzo;EeK(+l{vgCUj4@8Ydeu~8GB05oQ25RImkb1M&@LLe z@z~yt#=BCfficDje5+J>{ty3z7e(TGp5r2nZe5X>8Lz0f?*by|JMO^j4^LP)~fb%#=PF#G`+<2da4BqER~ zH2z}KW)MRRDWQ~7j(@Wi8A9da~+TC*o)j!e$q zeatbHtY>Z1P^GCe03d=x6XB31UEQ@zmond7Tv+T1(GeJg{MKi_+9p`hLJ}3Fh9H@a zBPZi?r%#%_-c-bfycAc_y;*a=lh>{t z=_`a~*>oKMAtelR2=PD_#2dSNU%qxekkN*;)#d`vwhRpJd#oy6f3HE=TOcIP-sYcc#duq zSxS@EbbBxo$1)d<7JR=RizNyk=rwJQ6SR0ZnIC@b^G^-*_ob)C7zaw*>s+xHj)YBJ z@7gA%jQ9j%5K2jcj9uHd4ccqNNLE5T?zMEi)|4sL0aMfnpA~HZ+|hsM z-B{nd@xsMKUh0&D>|`=*$CZ#A6G$u?MI5#|rmY92+4Mo()-|5zbY0&2_*pq{dcu%?S|(N&-4go5I_JS0C4~2-uBgvST36%8>wvW zIi?d$r*lIC-Fmycye@^baAMHumUmY70Rv`pvu|Rwz3;7<>$%aP`pzcYG2*FFv*mk^ zd+-WifcbntAqzZ;d3H-*7DI#q0O*~x{Xqr+0M9SI7LA7IN2UahyS91rE0UDKk*Y1cmcGN(qBM7=t31L+z3o?+*zQ zXZO6fJ!*(hyJQX=Rg|dU^n8dRcvo@_JWqqb_I(K9+X%2jz$jgM_5ALQMGtVtGU&(& z<>_UM63QdkbEw3@w+$mm5C903kUBUs8;N;43(MtAJtT98$IK?T_ig&Fmm3`(KXuGD z%u7!_$8p&AgZ%99!qwI3(??syilz6Gg+7e&-sav|Rxw<%*YaW!#XOHaEN=WJo7ajO29n8Wl^uaw}%E!O7d05urP)QG6K0YGJxxB3WFH;;MdQ81e8T( zT!;!Ut?E%ZG$MMRdt(d6^8eE(&pva0`RT<<&kT+XgfK?uF7L?#b{ff>3wwouf&R(y zNiDp-zTtLs+Y5HKa5Q2bTr^Vd=;t>!1)h6jb?wT|Zdj5ZJ2ofsd|mHt)M`E3 z$;V==CA*}TDjgpJhA@t$3+C4P#PK5qSut9j6DQ_F5XhE)ack>|6UUMwFL1a^z&xV} zLIxmX6d_D_83BL-n*{j-F3cx{5Q-4O2x_7v3B2LD9m8zf7I6KE4}Abr(%&`u6R~Qy zp>oZLBnzk$mSv9T4|=y;-&Z7QzJFjOnFI(QSb!b~Tj<0I7os>|RaQI?eX>nmIP%?@*kV>`3QAR!f} zV@?ev&i9}im%^#Y%K3|HS8jIua@H50X?D9<6YoAc9m)v(bN&0*ZyIe2FvJ2{-?Wsd zD5$*M_2Pwys7R4a#H_1{{=RH3g=tXfSe$^X)}GhXPn^|_^o9d389ueas0Sae|jPA3P#;{ z`Na!Tn61?K{>gDkkpj%M^tghp%MXu5q9BYJIpWS8SBI+t`Og90L6H zOF>7oLzDNsUk*hP$076%gJ(vApl|AAZF_m`^{>#t|8}_s2qC2`2m*v4$8j9S2OJ-z zGzdr#5C#~B@pnu&9LyzsZy%+tSQFW_ixt+!(re3?*3byvD^0xA^dN2(GGD) zG#uiaYcLFmG9P#WBMc(=gK^XhV%V!Wp_J@e-nUwFiANgSdU`areW}WE=v}#JF~-1~ z5({sCfDmE~TFq`;LH*%a(_*$qRS{AO7-OY28OuoPRfogyKxL2t;JL06(lSH+p?GX# zaU&Xb0z&aOMGpsjM7%HCe`Jc6q;p?*(sMmPSu9uR>0KIlebeL1uV3iY8>7eP0zyf* zl^RhuYc57_2LAvc$QZ=Oa;Ra_8WKg!D1!hn2w~v)K!cpX1I7qrs?4Qfx9;~d!kzih z`(OHlKPC+IEIO7EkB)?1Jhy4k+{lrM^~(!yxnLaZLKYxku+??$nTQL5%#&6w*#Fb# z^~m7V;9Rm(D)vX6rQMz=i>e46*V7<>^~yrnCYEg}k#K%=7`O)Lwv4v7-!sC{EkseG zkQjH`Pha1mFg91nDFWWEH!v0)LNUh?-^U~v0`2R2Q6Ne~s{=tR_o8a`sHK*So&A=lX#x@R}&*qtSt6%4hEXa9`%ijwA6X zki*cmeLKWVr|{rCkz+GhRiI(5zxc-PrAuj(-*CO1p265H#*`wRP)mY}45UNro4bqwLFBbWEE?h| zl$ws84T7n>)@1VLl?B`C49=#5fJK>Xcs+ukYk}sqwIC&o96PeTxI~E$C^jMgeal2|7H8a*1;X_afb5o;fDaq|zF7uI4VkBEy+1Opy&&R!P<@W z?%9!W3hCwDYoS};eD~ zJP^0}#*YPrsFIwD#5c>k2VXf1iVyMqdg)VHi%j45LvkcW38B7EfcK75IfyXMPaX?A zXYI8wV?kh)z4Nr01Oz}-$fr*ppB^15^!MjNA%!v4YIS$^N~`NTm#?mLx;@C)TN6g# zZllEaJa-L(0Dl+U$9J=9&kHyXe}B_~?*YqjTc65*81ORve8_$Ssi#B^+G!Y$|pGgn&-+bkb+HMi^yujg+>A~IgVq$nm(bUFnv0bT; zojmHfKB#P^LfmVcmL?#`@v&^WwO=$mc48`_0Oyr;JD!Twc8eU(ACwj`hm}a!HcaAs z0R=|~6cMq-o|aL_J*WB~ixfvQDu-cToWHtbXXE09g<^J~PmM;}l^VhbBY+^p5FQ(l z7mChQpR9>wAT9wYe*S7}ub~HxMpRP=hEs)X=vsx@x;~MWqmgvWbT-Si>3qJS>uYNp zwVh&^Cy8v@7c@1j8cfWN4m~hHu3lchSq?h3!yuc;Wf)~G!*GKj8wpz;o9PRguJg)5 zWo#feScspyS-ERs@^3x*$ZE4a9#*3~wu9jHa{UjNH#>UQ?C7mZ-Dy(d74is{n zs!GwwP%0sW!rVYi5>zD|kEBx*bF=faQ%6#14uF8t12ITVlD>TP>UN`Hd0tdek|B-b za6m{%k{0%gSGIR})c(@t%^lNSFBNZWt}X8FHvpY|{LyGO<2xSKR4$!pudeTI?Gzn{ z5()sIj1oe4IV`DR+vwP4mqFzEen3c6RpY7}QB_qG>)qaNvuS&t?fWrBx#x+;?*8~k z5>um+rX=&(>e{BS8*BAaEpS9JG${#$<5S^qRMl*s%A#yKPP5lKJv10gq?3xmLxM4) z4E)LYH$J=gVq8u1EAsQ}+i6LD-^t_0ve`-iFV%YU8iD{QsqE$JH>94&)QMtgU+~<~ zRBnH-h)F2lFYM`o6pBcilph>NzIXG+&5*`9e0*lO@6!33wPKYx)Mz?}We@kIe2GE{ zWK#(&@T*I!;dpFlZrbh|2=k(>DiMj}1VSh$1y;}WEEdnE)g)Gt&}+Bkbb9RMF(n*c zd*fQEw3F&f%-JKhoR?Be%nC;Jt;ZQ7- z4Riy0L{&81?Cv+3u@IR^+2y*Oo|utU86nIId~Ij1w6ZQLirq6@(?%E}jBoERz!nILJ2pQtJ~l`Q*)LToVHkgVBde5Bj^h|( zlzz|5!h#@3r4m691VQjUG$4ZzVvPQ(U6XV=;kq6n-zV=g#u&zkQuf{K?m*O*vG1#2 z8-y?j$nB#y2q8kq!G8|djYE9bt1bheV2E71{pP0*z4DGM1 ztzW**3xZ=ieZzfWO*=v`owVi#v-q1Y% z`t`-i&R%`5glxNAZ?zk(R;!(mWFCU`a&k$i;ehLanPdOaE-K0i8iq>wA{ zg7}s#Dui{T_uASTf~afTrB-LFUSF$JoFHf$=32Gl22{jsAeSh1nQycZ#)0b(OpV_0 z{zn3zKmj`=7|Zu%!r|dmQa5$O zv3};k2Mf{gXP0+&iaR5U-SNG(^8T-V=FeWZTH`q3>|in;kN*5a58pRA8`VOEd?Acr zl*e^KwszKfIN7fX4h<@{Ms*{7^l~tp@S4l-O8_TOm2^U8CZQpKg zZ;wh~t79PqM1>b6DXfNw!*zQ>I+o;k$p|dh>HrF}!^5uA4FW50JwiY@6^r+!QUfWB zH3T`+E~-FIk59{qh|#KUFDw$D(EygK)l?>ujwhB@ijgd43}=Ph&`@8oy}S0t3MB#N zxr5Av4+(-#r;lccf^5qOhvL;{w^i=CJ=bcp0O0v?u8@4m{T|c0!ft5%ndj_saDjNZ*1?ZRmxlTaEx#7*6nI!t}I(i*LR5P@SR5jW<1yFlwK2lq({Vi}i*i&A6U*B5h4=pVZ@%guK>ecS9O#@ICN`|m&d%fI~7BO`<7 z&tLieB>4zI&-1g{^e_J6-~Px)KJfI@FAohBe)Bi~{^;oN_3KNX=Y5x~@ge@kE{3-R z{?T7D3UsTt<$Fjp9F9(WmvRl@2hG*9x!22$$2ujG$B4b1MFl5v-I^86D&dse+BdN9 zgT*uigo!+QcsjgZbvJ5WR6*>v#$t?tZqb_VAL$ccUhU4En3W=t%GOT1Qlo^3vP3Dx zkUh5M(0Uz2n;FuXGMrd`igHwZ#Yi){k{o7Neyb;jrAb2#4SgpNhS)X}+-B zdvr!?8%`k++o*V!7f6B?3g^}?EQccc#IbxcnwFEHoMoXEwo)oTd}LOUBvOU83TLgT)2qbmp)0MMz_ z<&YXl#iQv&yHXP_JEwALDh&uNVlw*sxB4F0DxRHGRP-FqBxLDwXEK0Z>vFPmmb-gKBZnAH_9yopdqf!R5n-@^9<{OQW0M)Qk zm_K56Tji1wk^GX*28R+S2SSU@wk#qN_{COdESK#fhnGcB(gaB+p4TqdA>guuldWtqYPYsMze-v`06$D|Z|jo0}gaJSgO) zK`uQichg8q$K{5Q|^}=+;t4tac#JJj;iS6C-|J8yrZ9&pfx` zdW;wF#F_NmooT)4E?;XNJiP0x^;}%r+i4BO3Idevvc7O-uiWm)lAwm=AfSs|#$6}k z+ncnq*LwAZMMVv5)u9?vPd#v-Ad1_|tFJ%#OkuP?bnN(}Klc-Ri_0U&j%2gywU;h6 zED4?Ci z<^0Id=!yCLrPWrc%1a_A3V;E5DcP}C2&%X5Bo8@u#4hrckpU-~i zLyx`j#^uj`_Dj8<0pZsOkawaQaJxdmn~4d0=%KqGe)zs4N2dSdfBY{yJ4JLG|M=~0 z^R})5JU>4(JUnpi+Tza6z98_oJH8!k7GwOL_uT*BgZGS%4t?rVf7EESZtw5-%SQsf zV~K!~<{NYnAcUUhO-zh_-)*3j zQA&887X)P69)#c6y22O>0){b^B*C^l#u!5IdqNZ*5)Fs=_M*SQAtWG5RB%lnqPLNI zJRrjwJH6~!)M&YZ8~lLCSU>;>0~o;vC$(bRyS{HrJfsW|3PcVbjNl;%Ezj@S{@iqK zK06z9^^MEdZNuOMf#dmudd0z$eM>9;V7m|mzCUv8NE1dxNxJ^*OMp@=@UH8r(HQ3V z;#TRzCmt$o@AqnTjIko~5|8E+^sASgxXJ+l1PSG0f~NR)-w^|pTG)AXN)|9m_4#8b zSZ7URuS1Q%gc?al5h1=bh5+Pxz5GA<* zPL7`_^!F8)LJE3mtvi`cJusEfZTj5eif(nWAl`Z6=*3#~>c& zpz-p!Pzkc>}cHmEDbB^z= zSE{;gkEBye<#N+7M2rz9q*Lj^?yl#Nu8S3sPsif2Pb-(M51c)L1(A8?RORKLpTe)R zyD^6d;sajNm`{CI;ssTddXC+(OfMjY<9I;;0SGAd2*D7hBVh<&S?^#DhY76QSQ@+Q zG$8~+;M#V3XFsB<0u%eAsamnuwt74yVUZt9CQpx!%=G2|;_6jG;rovt^Abbto#hud zYakjYhAwb?Ow&f<;kluav1HyhEsqcmA>#Y8rp(-P$IYO3q*dRo)GV_W>l^nxQzs-m zc$dWsqRfq*&T_p=KlI^+YA2c~^slaOh_YlEj&6{2HniAvj}1h9gtm1H0_?kitNX2D zyQ8)xi8lA^vc|`A2|qBpO`0fVwy$5W@7Az@5yqYs#Pg$tiCM?4I-Q>Dl?}bg2^%xD&QHm)3iBday8|rgJC}5mjWf2C5fHRp=pA;nFc4$UM%fw(fP?ZnetX4sF?8 zomioif_a>biUZ>(7q7pNnT>M-pU7#GL(21C+Ai*zvV?8h^IW2d(tKj*#9+GKtuI&C zhpeO(*oq_pz$l>@fl|e}cC$S*8t+<>Vy*Y+J@W2Gmw8~TeKTM*noUdjjGB!$SJu)G zKlsQ`eH=pQ2jI+)j1eq7{|A2p33aP=#u(->=DEgR(dg*ekwH1EmDV>IWwC5(>g*{~ z@0K^W1J4UQA25JZLq{eBS+4EuA&enHydVGoI@S8+uRK3`Y%ZKk-1nm& zUORs!S;$#>FPcf0H?}CH5W&AD>m^lIdZvL9@&kWnaJ*}oEkhSD&kk(@5Aoks3o&~$ zm3@azk(4ov(0d;~d+PX%<#-SP^sR#^gb*Q=G5Fqx@7ym|Uq65So$t%{{dQaL^?JAG z?FT#sA>^(0f!jMN4l?#zc0+_Pc1ui(QVJnpj6M78%OfL$D=QndTH~!@hi}*>gn$tG z^+i2&hSm4$Y@gAh8< zz(NQTLJxX94t@svI-4+b&>I2(#0R#}l-|xhd;@11=9_CYgBzvTzM;bbBNDP?a`LddeLxw+|&ee8)wqy0O-^ZzG= zY;5d&>QjF-Iy(3Zzwo!upTFAcnSbS=$|3%{Vi{;U9UB`fL?Q}8$g-T>-P-16`K_gb z5E=?8Q&WTSxW@CG?+4{_Yjt(k^S%cIP=`drA->JAE|2sYc5*PZbEPgQZ`Z=g0OpX{ zc089vGHPQ-|3Oj91b{Ji??kxk(2Lua#6wB}p}_KKF2e730z|3nd5LraXPHC_q)#+en6kP(S2w-{7*lc7ZLL*KoAfLZWNt>e67g}VElNxaX-1~ z2m)q=$2b?|+<;LEcnlc@7~y8GC-L$}P8RlCZNq~J;$=hXl404VqgNYyH&={yXZEf; zL`6-f5|vIXJy`G$av=a9LaB*KIT81D8zJZ#)_%{dRP}*mt$|rAGxYeGxs{Ih$UWl) z9vE)@|MS#yLJ)8OikK8Q?xj*GKiJ={Q~`vym!uGaeK9_vahJAx)vkZQ3f*E)I7}%u zb=|Y=XgZnXvo3%dCv=&85N1&T5QG?^wrT3Noeqa~}Ahcx|nkc$K5D3co#Wkk@ zLXvQ0+lp%f0MPaPiDr{;x3zrknb$7Gn=2ofm1J2IJC@Zk56nRj zkCDK0Ji>%hRTMMfa9mT{mfdsgkSNDg4N+Em`8-BQ8|({A(_Y!`xBMgX$D2m4pvlSU z(S+;YEeN8lW}^{_Q6;~1{h zaUH?|&L?y^Xbp1OPOsK>Tnu1?V`)|O0vcCjMGKR_+q$|Fc#a&EQUm$IC^z)nogIKFu$ zdeMut+cn3O#nf=v3F!XyW?l^U0s#WSVrD3!7HidGclS-)5iELk-tNrI>T zaJ5`_y=iC8|7frwixFSUA>=Mw}VhS;S6#)vT%5P~t-D+L3&;Lc;I%QrO3 zq{%E>*{lOV7-dFBr<8Pthu`yKPvnO3K|mOx05B*BGk2ZYy}4?(^epXv4jq}vjgOSp zHi_*Z4nqJD#&*xpo2}~RPNF|Qd;gu?dQ-19`liQ&AlSHkof5*{?2`E2zH2l|DK)D; z@zarHNY<`y+~hDmWEl?e-&4FqsU``OG1GM3Io06%{>X6O-FF^8_{v`y=pzIfV~Qd_ zbpPq=3+tBSpm(-22qDMe+pFjhLd&udf}xN`2-Wp2LP$|$_GTNP@B0U19YGL;!y3k% zVVI0jNs=g~vMfFS{HxDB_bOvd5cq>%ju0|Ui{r4WDwHx^??MPQO+84AgMi+WgY`Vm z^COWk#>g~nLWnF&j4|I2I1WpaC`l5>VM2)K`QMNRAMEs?6_RaNo*z%(s{kSyQ&0LI9&oFE9o;Sk2yG%e5b zR8@J)Bo!fi_UwtNsgW;!@vG%>4I!u~^3|&gOH1qX^V4_Vefr5KpFN;X4xi$80SKY} z{mJ9UCoo33ZZZbKVP$@PM3RKX#jS%-Arw;1o}CVb6vMC$!$3$ z?w!y8V6Uwk0)_|YhA^gEK`th&7JJdCmKh#QU zfFbn#AU-t8N25|CyScC|=;hO+VE_OEER~H%WXnR``TOrI)pr9g;34QSz9D96ZSzyl zSDFUp5QGr8J~?wLnY6vnUFuktpD3E5HLHpVJ_@h2fzB@s&WV(9SqTcKne(ekg`Hl=#L2-HTP?FBotas zCl?*s?f5=5x5}k6V>t*E%=rug*YnL%ZDFqjjvcw{%;`(pD=hGR!XRV-B8*X|RyTSU zLg6p9lNCE3LIFMLG1xwf(1YRzXdYo*HO!g8iRAI+rn zT7yw=aKa%3p3lyXs+YH|<+2l&F}Ym{oZV9UB0*5!ElOdnMuM@?q3Vr=zz;YMSK6Ia zD7@Eds-k#qedB{Ck1vqfC{P+0r zV`G{0dZpUvk(XCjhmxtDC2S(G9Dg>+5O2;%Agfd_!GU=sP&MShj zy1eR=V5gRP@9}}bw!gU9jmq*)qiK2`2ftQch5!&s14>ij5Q1pC-h6&(DIgTR)pQFX z2?Fl*H!}Ggk9eb{1IWT0gPZ^oT&vp2Qm6-ktc5Mldv#@Hztzq}!kK8~m8B&TAN1?H zPaG)}Mn;o>R&>2-246b2`oi+kr~l5+ghFA@_aJ~CrF{w)zPh~f^0_?DT)ucUK7R~F z+!wZscR%t3P#~iK0DuuNEoAE@m4r)sCNTCa%N9hA7jQ%Oa~aVDq#W{29Ua(mT+{W# zqLfq=8%Z1=Al`%=iuHx5r}-|iyRNAlV<(Q=rtUkg(W$knt-wZrVqS^(F6*Dm>|UrE z4J)2cD&dgtTl>q4ij3z6XB4FBw2L4j1kQ|%TzcbtjnL3|TnPz#H|k1E>eU^V*VGu+ z5;0=*lDVjyb_*FHkY$ChQbscqanK*U@PFMXmMknVk(Wnu0})xHfL&T!)D)bFg&Mla zAr1n_0Hcf&p9nl3RUO#hfB(|lGk4$Nl$(2(7g<06=Dt^JDMSbYpop?*TQVFQ<8y_E$WvNoBzxd*7H*c;X1dkk-+gB{H+Pz_rW!b4z;_=5H9T^!+rQ(f7 zdwYBD#TQ?1w>uoiK?rTzx$CY|M~}`F3VBLtsZ_ai>DuMX3yLBehWYs8kIc?azWnmJ z3m2~OJa>?yzyJLYPfm_K_uMNNFJ4m=8A8Aq#Teax|DBIL_E0PqwJdvOW&NqAUNBAj zfT19S2!im~V`q;apNmE#y`HhVTe@=P`i&bak%;!q*U@pDNF@B&V-Jmt3}!N^ZnwL; zTReB}%IfOo0pmj{WsE)c*n^XkW4T<$vh4l+@@ubMSY6#bU`b5Vnw%UzbLQB-f^4?arIsxV0b&zfHA}XcmXh7>iKkJG&eJom`%}kP_MSvGa2Q;jnX4b z$PGRH{3U@>&<*by($1A@?UJtL3U}wU<*VzK!&DJL2nYo+ML6A$Usy7I!Z-{O1PA@j z5P(~mDFhe-i{dnA%DlQ#CkIC$3Q!c_QqkI*i0l@16pmOvR(UXx5D|ie0S18OQJ*r5 zkw@u);$_Ps9Vg{wW}Ad z&WFOxbGH|k?wlC&WQ`Hh+S}WH<8{Y$<&Z+j*O~)41e=C4H#ijlt!GyGj{T8ikHppJ zN5_xs-*eCZb^Zbi{AbtKXYabhHcZ#Fv_!1eZ2O)k2)yO7)5D4;;#by;kc`N;_8UVy z=k$!_rIq*JbvD_TwREG`>U5dY7mHBj37PUcBcvtL~^Vk$3v{a~d}h$e}dNJQYcM$f2s zyFAD72p{yyP{w=`M6`%3h|Kgv2@0C1h{8xL1|*4k9`!w!ad(c5*1X`qy#5BIbhf|G z@x132m%B!9xS)8dA}R99=1!4wS1QKL#My9!4~N3K>-j{FY6`+gMe^mnmF3=MHUYA- z@bm9~h=X#8i6ebp)UN1sfWsVskTBNVyrz+6{~e<#v2p!+l@~-KFezmm&jTBc4999^ zn;>J-0{b#N7pb$GY*=9iwee z+|@UDGA$$C0|-HGb!lgq&nM%s<$=7+Q;z@y1BVC-uMMc&^~UtTxFX74;BQp-8hVT8 zc*?k2FKk8w8c;?fYD5u5N(lo1LD)7sM%~eE=_3!1w9RNPC7(W;ICr@b4hw`bQ9zH~ zKfJkBmKQfC&YbjZLJ1XRX?Jn?;+LOs9oM+k(EuSr2gbR9@0%UH+iYEX;pJE+mmMD9 zMZxYFn^&)I-?*uSwM1WDjfO_%XI%<3%ww zkXK`oRigLIK7Tu_Ng4agxrT!;s;V?T+8+eu+mZ%^ z5I_j0CWhX)aN~Bhg?FBixUP5X*zCXf7r!w!HiR+882Ns%v$OwOzxA)b`qdX9M2SS~ z@BO`>)AeqtR5^NdMwS)Zwt|3u^rIj6m0$VC%ggJwZ67;!gfi#Bm(P$n*T|?Y-ant^eOwzxq7JTz`N5=YH;!vMj&nJ@?;#|5*UQ zL2JVkPrT|xWiUV7<`P)IxAci#8Dhko{FKRGcm>ieGS`h*12(-Z&fpZ%MIuQ0}b>ZgA6 z7k}}m6h-0rTaoa>!qUI|m;dg?7hk*memEefQmMqR{L0_?*vB4MRrS{B4Wnt8J_tR7Gvzx>NT^V2{5L-8y~~$x{Pu7E?-ws#6-D9f*^~e1AN|UyQ}b^P zP_$Z||MUO+&;Q{+eCA-rB$yjQ@dXADgq?~tm`kZK$?kgS z%_9H1FWPW@Hrpq~Rqm?`U4#Hex8CuLG2bV3J$UGz@hg{u(&|R5+Q1wq%c5=tJ%<)z zLemItC4K-f1_%Qj0|5o;5I33(C1SFcP3QnoS-tq>bDK?KIHc!L&-DgpCaY`pN~L}O z14q4f4cM3?HU~jmqPYlszG&EPps50-3_@`Cn6j#i<+hJ8^nLcWM-dPflDV-k+3GMK zVh#j^fsusJwTTt5ukIkX>g;au;g|$C-0lS@29=PEL=N6H7JBN2K9LdP3P%}}1@y+I zxz%tH0s#dO0>`I)F<~Gbd3K@3BW1QP>-g>KduBEYN7Ct=`^A=F^u=OJ#geAT^^P7f z8~#$~tG~M!JASvQ2-m;#7o<^wyuc_02Zdk&D5Z>{Uwq^w0qBokUv+%_KmYvS8qM`v zuIm!gFDoA(8U5M+{OP*q%Cd|_A(}~qlSy6_iz}kM??Q5DU}`1ZyF{BEEb7NYPi#^w0wWDSn^yN__3I# zs!XkQ3?xe9$>_bB64$h~_8{W8TD7=Q?!CTA=K6&Dr$Vo+`<7W*>R7mr0`rQYo} zEz=K($Z_OghLZuph$<=Lxq*-@06;z87ci6rt=2UzEU(U;IoUTerUYxplc{r?n}2+D zVZOgGp3N@o?=KaL0>|?l=M%zn7zjdZzdAVD7wXWZQt^lsoEjVdvp3F-DDuoyj$-kd zrScm)%L`kZmA3QfvFJbiz$1HgPJ6ZVR`Pa+)m65soJ=@-2y}}wRtD3AYmS=|e zPDyRMq7n}A9QGX7b^UV78SIbhh9~;maXxji*5pKaK0Sn^nb%*vWcyY;lkD^?y=FyI zDUKJ)n|u4KYePq7q>$RF_N-RTa%kVTr zD#aLJj_d6R8=2!;@p{WM{YXw3I+11!00^z7-7o?K1k$ySACvz0DLpKTBNAsi1VI2G zV3-Z0^U`?CpvHEsSZgyof4Ixb9xqxm@O^c=@?yui0yn=ytFR%Nl- z@xS`o-h-zG5b}QMlOu_kP;0tdyT+sUX0P9Dl)GYlb0?8WM*I4BjLc5w>a#ChedZ-B z@PGk?k*LVBriqFS06-iUFJyCL!@MB4mTen`@3>kl9E!(E>zfpvbu`@%7{;&I+>LQH z(={>E-QC@$CZ~Ivi;3xGriNh{SI0Edai?~5cXxAt-`{_Z<2cya_x--_^E{s?u1zcO zV;+95?HgtXdnU?bzSo;^hldWBQBMKp79)Idb4LLEfJ2Tml0g*(yww*$Hz;7)c`dU1 z>6zhc0kN!rVpz4=h7ZaDALB!gj4VC)EdY946#Pc9g3G9^G(Uw<7Dg32SQq`X%z6_h z1!`*4?tK#$PH>uXEPmQKs42xP_fwo~+A3tCxLQeMdj zY-vc9mPYG1C4;*7G$AL7_VbVr`T^oT%lDK`=VK`yR}LLo6?|iP9o;152r_VfQd0gh z@=@IHc9XxjxKHeEydRK#)gPx49syaYS4ZfbbKNlhXzd*n$TvBTuQ2C-BDwb-*2Z3y0oL>%$|Z@^OMrM7g=~ zktqFqL3uk>-@d}$r>>|HEo+9fpD(_~7 z9eE}$r;wYM_XvFFDktH*+mY>D|D&`3v3p?VoeCcbJk^TdN9`ksMd8Tiy!)ux*(hRD zpU26(r(fH*$AQO>RCj+Z#qVqLnt~p;hS~G*(Z#Y<`_`*_d((h54g4Tk8^QE4D{Fqy zS$%RUKmY%quYhUd*g{nN&vLM18V$PYq!=`4@cQ#-hgYCZ5}Gj5LJYYoNcV6Cf~lzC zu$95+>5cLyhnn50!qs~bVE`uq0;N__4UiGL(jHAa^1iWuF=#9FRem{i8YA{^pBld) z!BV@iG&#o7rzG02&eIv&_asU$)!DBgsN|to5`9dC1n0)fJGKF|vMoiU`~EZ==fK1T zgI_7`JfvTS6t2bz9SNpty>Zo8GuK(&!aPd~ZEbR4OMRpkuQKH64Oy5D@12frfpMz_ zmj&xcvS8Wpvze{?vPS3xH|rJ2q2ftYilaKR)CEfZN#$I*x#-&ct2Q}_vIcUSZEUS> z3m$!?@Q!N1eZR5(dFZXf zmKBkZm{FxbYL&Q8;p2jPqd40?zo61Xj8tY+Z+H3z6K;p!jAu{A9W~R5&&Hgy^bGV~ zo_<}w$AfFjp-E*7Z6Xmp>&V+QG6cJ+KKrleq=7$E-^q!uM?|*_QZ<4h*1yD)d_29d zq#wmwtLFH!{okalXSR|-q?n}!7_2z*YROAXE5Rgmu?(v3Tn<=hc!fFjNXSH9?>Xx^ z*?7`x%6xfdDxu)|9U}ztyGSz7wT0>d(|o>W9o6hzde69m`}Y-GU^cmz_nx;Bqi66n zQi*ca*0;QM&3+TruKp@!OnLP*f8itYU_$0ipy!8``ua~v>%hP9c!l3%NG9{}Ie$5? zTq*TXDBe-SK7=EQ81x+vC;jah#>dnmoB5uh9|b|HYrRncOHP^m4Gmu`Wvm2Au`d~A zR5jJ&f-y=y9~Mv5zw49B)J@*@$^ARKWSEzx>T52g#g<|2=I`*Z6NE^ONI(*&ZTK0IYFTG*m{qQ=vsg4!GD>hg69p8kebG`Ysv~M=r zAFi!}d?F00Z=`{C{zfjIw5LCbV&+6!jM!gB|508WY7Sp*F7+|%^Cq`qaLRe>a&()F zaG3NF)A_0}5l@l%GD-)#s8Ih}3HkIE>Pat77;UR(3|rCi&?hEj6C|-ey7YqmLkUIG zR$RDzu~Oh>ey0MI023HJ+3KCL61#$E2B@h;_2&O&TzRS5Ysx*#1>nRWpZLgN{3=gR z$D2F*Y0qpuSRomS}4X-!9~GntJ?NB3z!$(#z2yF68Uhc0Wzzea#cM*cNt<5_7_!wb;Q z%S!CV~HNZKnZm~+pHA26Kc~`>V?XvIZ_J?A-cUGC#sABYBqZo*8|v=om%ZMErYax5 zE`Uv?@NU6-dA3mgW2H&UZF$FC7H~!6k~0gu?C!Yb?+Dx&n4(KqxSvpV`4B4rG?zW} zLm$GIpAM0tVAR+o`lybZ7^rZT>gM@uTwH}k^}|We^-xV+P0eXIy9i;N9QMoFl`KzA z94{4SU0oo@0g-B|s(RQ}M*Xn%^))xxp|{P@GxeuWwX?;kl@(5peJ&67JgJwq% z`(r4wXMcYl0^DSm5E=Zc_u<5*_ah(07^AzJJphmT6|bs8V++O{(YP(TltBgG`-(B6 z_gLiphlgn@lksvrNeVj@i58JJ$AzU`p8|*NrG1o-rmjB+h><2ZELKiCdI@54_Sx58 zh3xG~YNxR(7ZwWkxC&9Abssk<1OSN)MhSAORL<^B94kxQ2k6ZGv8`fFiweOaTfmR& z%>H}rk^o(A{d$-B6#3`qsOV@e2Gp-SLhg^Ox5ip}LP?J}Tu6prQ#!{=P-jE58mF|% zN7mCm#^hbUXpk!#nio5ZCd!*kc1sM&F!KAiC^+KOPrInw!U z8HvqmoSo6-rH~@i(?v2H!RxlBUuHyVQepNh55IZQ5zbfn6ff;G!)Cen^loL!IOz}vVZi;INA1rJ-hL- zHYIA5pWcs7cyV&X7NSX`?Rw4|Bmbm{f>JSXR30vRyoW-c3q(TDQ-?w|ocR12`f>Gm zc)>p{-v@itPCDtlJ`VVB(d!_mVr9?88;BseY%?vi_WYgA33ns!!FmAEgx%e#Kv2if zl=vMkcLU5D1zo&;x2f^->yVI;{>mac4+MA0wFUM64K6F&tRziN<7*5g3k&F4Gp$&> zL`9B8Md>K;o8X{Q<7qiJQm_xm(QYIDrL0zwtC8gV-fsre=>9>E3JsQ{t2P#Cov1X# zcDnkFP@1;n_Q-&lvFi@g(!3j^ZD7HYT zYR_o@OeIo7@glc8vYCs?c3()#Jh z=9Nm=ADnETl{_(WQ9-V~XF3c9jGzyvJ=Y7hj|U#Fn#pf+CUL}V|3#m&7O1rP))tpi z7om0Rv%VvuSpiEcH5*0-DXBz;hijcvprMI#2!1_z)xW!x7o*k%kY5W>FpLnyAar|~ z##h7k-u1Rk?b^8lZWFdf%EYjpp67jaCOi|LlYUCQytZK%YYW^Bcs#LUgEIM*8Z7;C zA@lQhWnG-v^SVA5vBEO9<)NU(`=U`|ACaJ3-8A7SRH80p=IgAjJbU~w`1$)}S&qve zn_97p(BNm3z0c&hj2-cgBtxs|F>9!mLv82O5S)Q4Q~s(6_LP^@=s(O(y6-*FgFzZY zC>Z_N;qg>hEn>Qa;C)}>s|c>icsiSPG=~FQnEJ$Sh4fL0t#+|1%yo=f?$p@E)n;vp$|rLC=zz77C&qf&W54Lo&#jz; zH|XHhpC!wvXu5Zt4udXJ!_9Z`T4dkPvNYN?>>n8!70G`%MX+K zl@$l2$r6Ic<>C>#&1Rfk0{!4hVlwE-EhfL-TYG`4e`c#LEmV)H?E?J#ah#@R%WN6t zjB4chslO||AMZxBOX7hzhK}xTq!6hkdq+US;!*KrlwX_gdY@L~d`vHGpvgS{py*!( zdL1S!lwd%5vx=n<5s(HuHwr5&J7D09Vd3CN0nd^^rL|!*v3cUJb*%RXvoTcnGvZph zx|&w8icCMQuX~c|BYJ35wLeqeL^yC;8G4mZAKqI46Xag~f4MGA=@5{co`6tE)D!8@ zX7u)LlETJE=oRDS?$q9%P217~KXQv7UGndz%XjhosWq+w7k}{8frC={x8keoJ>W14 zu&I`TZBzvu-)j+w@+ATKpgz254@EE6tyxOVsp1t5Re1u703uyTor zFbx;%-yM1(fY<{8uj;;6oL1?GAlzyKnB`RW7bSTtM8m*KKs8g0|8p6MR6Cv}GkaUp4oHo!zN3NCuQ^THT!5$9KlC!yw%&s-^N^)V`4Q^PJ-AB>%Ni$a4_z zPPz^IBs#RtqE_bm!MSp_KBA=GuYT`pw6~`puWs8N+g4&)!9XWf&MiR;C`h61ZhQZk zbBHu+WQbmOW@*o-t)c&99G3x~DA#ZY7aiQi^W7yr@uHC*B8eR=u~KcmInm%S%hqK; zx2$- zeeFEX;oWf4X(d2K)oi*iQ{cADMSU(S32`&)O2LrqB*i3Y zsTy2%V>(}2AF9R7aS(1neg2J=kw>-D05>gn|3%o(f*n61>k&*QkVu)Gw`y!Sy&wg4 z3@Wpi(BNdhCf8Uf2v5WlQL4zsV=9yq(<`U#HijZeLPNr_v{M%tTUho+a=$a5PQD*7 z!_QUw+0c_et(;m8+A`%WljCrNSVz=fDZ?&{g`SXM7+7k3Vn3<*hSXzEPD=Mzs z|L?*P#~UVrlW)Y%jvePA3wvtb0a{7jrK8ERAhcla;G$ScxWgW}8NZ}YcbZd}1q536 zR*RjjxjWb^W3oh?#x@k~yywYre^;XIV&=CsGSjsyMZ^9ayH?~`@IPl*-6EM#`s=n9 zjV3y79=wuh4q__fjPMJeKfhW;K%uc2X0wF!*(5I+<>}}cwdVEn)6*l}anb#BdpOqi zF372PZtBYu_kOf#8ZFHvu65r~2-;Wm;KVk1zbjuurSfL6sh|8#{ej?XvB1~(p)VTz z-`TDmqapib%ND3K5h!RRJu+Iqx2=-7`UJnH3S*Ccl=g_s8HS2c4Jw*bUsJf!g%6yz zHW>awo67Rv>D;Q9-Th~?^2!N#AKM7z!$lb|Uw&E1Uh;R*ptXum{d}FSus;HOL42D; zuC1v*l=>v}y&H3K+{VYPfc_L7k?WN%o6-e`Y4=+KiO+8{##sD(b~;h8B?2w|FE@!d znZ68+H@5OU&XuM}gXqwPG-#qcH|E7;61iAnv{(s>QUepz>hEvHj8LAZ#%-*NKU}S6 z0yd}#za9(-1#(DXM>?{&`dF;YB=Gym_by!{W@}SxhLp@jI%tQQITl}0`yrH$# zZM8dXI}dsZoc0E5>Z+>Fn&G1N{~sA6soIgNk;gEdvJ7^32jJdd58UlFh25uT<)C#c z7nRw~{jR)*HtIP{?T-S7Ho!!on>Ie4e|Tt7GTBza#2js9X-NhGDz*bA_nMdIstqn4 zkD=Rv*NeQu!nq3co+h<`p>W};BBsD+YvdN#zoz>s{ZD{{C*pn?8DCMcR-}w*f7s+^ zZ};hIFd3Vrdg#h~0;H~sBs!p$PkhS$lkz+t_z*~3~!Mh1dVHX<=x>UG%PPbSOP zO9uGvVTn${k`Rd0-E|FY{A5u_8ii&@xS3p-k2J~_6Vu4ZC~f|h_0fu-G zmrkiphp$IVN9+v3_Qv0pQ)Yu`9MmE2`#hL)Z@OZ^QwNgdf^o7x;+Tyl`6nlubhK>z zm)`Ob-U}%4z(-mzYl3xmH=6ccdAZH-kAh(1dCi{uRkX?EV&37jv$gXklC^ulO4D=vD*XxS#!* zcRnDgYiQP-&7Xn>PP+#*&HrlKpBYEHUHHgu8oxicx*8H9v$N%V`2hU)b&in{NYt;@ zJl)-2%$*JidHrr9(AZnGyzaN+XI*dzWsDp*v!S*i$WH`SAPm%$uKPC4X>#wEz( zWH5&ft zrywJkaP4&4dBF#$x!Kr}shB4W0Ou=_mSmF_M^U$~`Q`-QiN!=%Xha{rb0MuLJH|ne zZIJ{4=F5Ndn)IQx;Hauf#R;!Tw7)^y`8!gAOFOp|pq8u?o_Wh0O-FrBwL1Na? zOn-PP_635^pVP3jztf*TM2o~;C>hFLiYDNM8|_^#_ysO{T8Ek}dk5rky?KfHK}TDA zlDiO<+o<7s?(88h+1K04b)3&*H8Gsj+m<%1b9Ldz7ISiHvenV}yrkV2N~eV9VHV7U zt$A9KTxb8^*85MkR|J>~UY2bJ*R_J5+$O)0XkC7(ng@NWbRQ$1eCORd4iPPgVp|7w zw{L~1=P>QQd1I_Q8TAufFynNo94>S8tcZD&shEOaay1CyiJ0Hz!g z?0c^jj$GwGFSVo2q1824bEf`qtJwZCA-t-^OTl%zes1k+qKmJr5?mzltnH>ksfwz; zQGT*}Zu4&C{HC+f3PLWcueq3t<$BX}P+O>-^&4ObV|i0c1NHO#w7xU&n}%{0vfl zc!)i!LSQ2Y4Eh4v@nHAo0^8YIVuAPP*{T`ht+rig>57t1awrRknk1NxaD~X)R zr(uyT4V`m#(SwYP@Jr%r>{89{1>3;3g{2%SaX+8aG-7X;2Xnul!C<0yd)^hJ?$Vv; z53f+Ez*0du#{)Sn;+|02im&nSK6JSsjt=q3ksP$VKUxp6EySO#k9>gukxp+UNuKjtYD3z$-stl{TN3TN9;6ZuFpI=*pl ziM6m52ZVFwFUQu~MjqA0nVGBQRO>;(mcUtP&v%{W!x}TRvre=Uz+_ll0z{+xfDe|= zbNm0-kRAcsb&~aIBlP}ydz<*U^?c^$W*}dhJ3;*E?|Oo`=h?<8^b+=DSv&hQcOMG* z4uiZ|Z=C4Y$oseH@>Tw-oc&%XK3j_gjNTnItpZhujOFaR$;zR)V>FLNZm0*{6jD8& zQiJ~?8sRxD6Sj3VvloCha2)v}49JXk+?6A5M=~)1Z{a2E0T?0u{>%6ON0X2bFyOAK zkbIE|7cpT&9RJ?G85y}bIG8HcW>m{-Uw3k8xdx0F`uh5a7c}wfR}KsuZzLs_eN2rB zp2cz&O}S-vymDF&3_fN`QPv~?%!MOX7Q@3W{4UOI&8*;VquT3(gOfip`KHIF?X7zP zOUu#2yJF)f6>y$dIxL#)>{FK?5jQ4d4X&&!YP0`R$`;Q%INzes{BpTWE?hP3!8X3b zX&a|TSL?+8?YgY9^H8kocuR}Q^Zj$JHb9UuWI1MoH3J9@ybt6k&VC~owzM){WbL$4 zV_hnGnS+YG>Civ~Tm7T3Tv{X5%V#J828sOpY$C2oUrJkj=+c$VpikEQHFxixqx=Yh z#PkFG=kE<8!AfO#(eha_8JrOna{Ck|lZLZY!}D)Nep(OS1mB%$!CgxgG$5cG!sg&A zq$72hGsx!5U8UEobxB)xhFe1E)S>>nImeK>n3Ry-Uk?F`-0>r^0H)Js%u9HiX(oxP z-H+UtFHpB?g89gTxMaMHmaLRw_v!Iz*chEbr5_pV{`RvY1cQ4NImu}B8=Uw*(9)>( zOj-M~=FI(^W;x8oC-F`wfgVPsspzQ;Irq?dB(YH`#0cEYHO7aPG>M<@QfAW~7`P<=32dd`8DYK>*w>!E~0{Y{7su z+7Q0DGb0wWQ!s98gM`icQ|L;Pi>4PzF+GYw!y2O3EYyTnltXEQXZp z{z7(>=Dv6RR{_cdbkVJKwZt*mcG-uG#w&D4H z)|UR)&%#nGgQORIsa#;mH+X~n1*xY1x0L(7JE9S>H@30SMugoY73f`G|M7Rjwm&Vd zj0%t9hzu=i*R267>s!%&Ht#u3)xi0MkTUaz=*^1@^vggnC_PNh4Z$6&Y{c+_2ISHv zg<@@I_&RTj8D!>nxS-U?O`f6b^za_u7v{5&RG?|{FW{(=pFisra9)t}d>q>9g9VQ~0XXcSq{2~E)(#W{5&Mp(A`bnQ$z_2t{$>b$;OqZPq$}e+j zh+g$`*)5+R=H|E|lN?U#yAgxGBF47`kTOUbse6`rQ5nEok`LuWS#=&PzWJ|CSp-iZS`^ z`6&Jnx!gx`-3ZIwt_^agE?V|$#(+A^vnYX^-7HUs(r%?_GDeHvFv|b$OtA9u+KUX@ z3Cw#uASPWewtKkE6Tb!MC0JM+z~dW`SH|S20#}Y&js=1q{?5(Wxw%c(OTxy8{|yf#RLE1ahWSPJ=DVbX0A$I%H%dyz$|2tJA z4qWVRCr}?YCSRDOS3XLjO(2^rGv9Derq=hh(jk=krhVDI9=H2`@v()uFa+ZMa3hnS zXl%wl3U|3sET0#JQ3>aLd;K+s!oJ=wgYDh*qajjnpg6E#b)JB^i4eE@#GmJHH%fO^ z^E_D_-Q>(3V0y&Gx8gb2&|pf{dN153elembzJmKqoCE*ca=rewitZbGdM-x;-1T$F)(k;7rtC{fqPO8tPD9u{2rY7Hh(RK$p*uo z_xKc9Wm6)`+P*tKPapoI$Y+xN3iDgo>*u{-7ev-%vx-!VEh>ew52h?sD37In&j0XA zL5MeVwB6l95v$a4cPjEo>;)*$*Uc{9U`+P(J>AVikqzsw{`I?y-3P-!da7Ub<(E24 zo{p|3&Aq)oJikrL8Km!!kL}m0+qJl|L3zg=4eqPWw{dhmhrMkUY*U8XPFHF$bs8{!E(uS3@3e(+X#Y;_ zapA^x5K}B*W1uC0Vw3zs7}e02@qmltyMKiRTDx92udu*hN47i=4GqoIIkPjqj;J%a zX8c$v>)FLcyv_Fh1Z&k+Tm*4A^jfc_loUioc~#I2X-O=}lL-wWsC@%+(eQawPpE96{ji&OlKQ}zcEK8e2T-5S++E56IG&@xlY6R zf5#)K3_66UV&tR-o2Cz6)C|yoQHPcXzTPy%D71BAmfm@qAM9eXiv^w*$uI$DMU&cX z?P%ONvrh->!i*)lc&>1FE4=NvZ-Gw1e+9)H%79C!zx7&Va$W&LMC|bbQy;dpgE1n0HxBhY zZu2&6a@{mT?jsj67z5tAnz-2hguDzlMzmDqAuq$;^{oC?fB1s~^bU*OZe-;8tSMLM z-Pa@7a)dqwI6KRwGt7Gd%;|oyn=bFe0|MkQ^wwp5zBfGFx^D(JBQb*uDjl-V{n~c# zpH_Q-5Yy!&H8>Pd^Vu_oFPNWQ)A;- zq2H(X4h~ZL`sIV}O2lhgTgP`jO%Q<_N%#L71`JiTp@)c|^_0Aa;dyurClU63AS`lo z^OB4|olsVZ-U3I==Y6tFpY3w`8leC8A2)Arq`k%LxHO_NGh6y^B%owDf!%=Ijhm)_!%I=im^(6qG^_m+npu z%8(~}vn~>RU1q^VvlcG)K|Bl*VWFnRX0m@$MWc&2R6sS9HVDMys)ME7lOI`pDemZN zCa(}C-ANjZV#z2s?I8Tmlriy{0*A`J53J`qH7B+^ZvMx~bkf#C39!c9-M7*`u806W zI$Z}7&Low6KJ=<>4U`@w_5pd>q!k(ZDW1<+Ejfd+Jh?<<&tnD*2z@$lEM`B6@+Cqd zQY_}NRB)D<^wdy|dmEPqNwP{_-jzO@cfc||XNvgoBBC5H<3yL^MLK>&_`NPc-1g4y_fsooKBZXfo8 zp}D$7Z)sZz_goVlqWej* z(-gu~P^azUIK5x*%wJ~LFE0<4+D7nWV-x5o2AyUS5A0{i$z#x18kJ{7YHZHYTPd` zjpCVy*K0FJWPBPol(~*3u3SuvgACLOldFTzzjnLuR4_Cqrybf1GwY*)sI9X(K{TCQ z={l$=jHrF+N350L(t|&zmI$+ew)WY4Hfi&nc`Rz+oeSO3_$OP3@qxG}wjdGY2Urrg zFb9)&8ZAb>th1|QYikn7Ci}8cM+S3bDVw^cS&Kbp<2kQ=QG56k*X}W(nIlU zo8;)=cD)x|)Q`ja%tuO9si?T4X2~i2u=69OOs+JIwTE`K#v2#uL+-rxnxGffc&p)r zEe_8Mf}JyRFCw1}g#?d#75VOU{wA)pEQ3+e&Ct+ zLi}_ho}@#Yt=24@o%z;LDkqh1CBiM_d60zRm7$k{c3W|q6>Mpu1*U;?xfsE4C3l{+ zSK3C(*${dE6Vk&0&2R}pK(QRN{0$2jH}M`hkQYapOH_fYc)=@O`1si#iuBN;;ge=e z{(o;>dM(#rhcTfNO8(ROW5#~V{*>))>Y!yNKllsD2y;>7n&7 zWo#Z`{Jn0!J_T?uX18f(4-*-tGmGzXM0{_6oN06~t=UaCX5~ID`n}N+zqjI;F9f}D zn)F@~A}MkI4+oG1t|*Ru=DR*~BafCJhuH&GZ!O%8FLv=0MBFMunKmK}Zcb9B*Ku2jiZf_CThKnQjfF$U{9G`jYEkJ1PCCMrl6fp2Xv`}VdW|EMP zu)nJq9Z^|;A{Rj?z#|)7G8iHhc=xANyI;F>qZZyyfZlZtMYhz=jsQW^+jD(jm_0lc zSQ~P4umAt^EW^d_hjw;WBE=tqGzmoDErIo^S^&+Q=FseEI8WSdEzz6=a7DMT$I}6b z$Gb^&&7p6jSAc!{U@#>TkZA4zpBb$6;mqM%aqBt-j=FjRJ1c9Bu(vFMtdd=auB_(5 z9E%>y<>25r5K+2jOsTf1eayWU@p`zSnXDrze0=bdO``PMIii!_0`{@DuvVg%yHrmn z_jI_A3(d;I!w#i7XEW8GdwTxQ8jB;(8Rrkj&~UOTDJY!zj1aJ8dUWo-w`sqvXQidX zt+?8on#!2CKz~0yR#&gO3QRdRC>3$_`9K#Qk2FFzvf-m~*x1E$PL|${L@~RM#?Qg| zPA%!1uGr6?YhmET--#_QR+z`u&Z2LV)2#dsu@kztNQ8=$zWQ#qeb=VO8@yGSloMMY z-N)~i8XSE0tM=oY=hB;LM7V#&Q)#dyB?Q4t#4k3mm}q_HL;l)Vw=B_2_G3c~-)6A* z?;AmA6gz1$ZSk~ZnJ~e05`tgKZyp&f#Gb{v4c)kEwK~X@dzZ3Gfk2WctFtGzN-8@u zo`Q0n*N;EsHV&`$(o~~3gPpDEJV4Z8)jG7UCglzsVLh3@dK3zcsWOKb1wS9>U^k1= zk)VQ^(a_INV;?Sw86groeit{OQufc&WsI1KZn-gF(fPxF{spYPpJ?b;f8(~UYs|dp zB!+!D7A)&6@Zl-v@iYB3C*o%nnrlm7zFMew}4TrjZu!Xn=Sy*i(HGhV| z*u~sV@AMY6W!CNhBij;>WQ9D<#%H%KOUL#Zv|xmpkG1RflYv5)miOgip2y7Nm)tcE zJtfEPfqCvr>Rbs9oRVNgsr|dFXwaXy=ajJ}7T?yi|Lu8go^lFtwbp8v)y<3c7sUeE z-@YHmVj&Q{)Nkf~k{sDz;Kv6n$#u2WJ;I`h^YUU`wKpDgpU1l%qIP$|Z=T4;tkRDc z&Axv1g&r-O?P2m0Ct%?;OLBZuk}VkQ^bqa8AQk2jA*Tf(9OFCBVxtQVX!DyIGbUwC zl2s5msJRJ)9=AUZYyCgINy)D|^qV&I7RUqzmnTal26LYMT;B2Wi(kKM-^5E`iNx>` zBPt~;2qv}u?l3!CrhhY zBnw||8H@3!kJkCi37hAQl(bcbKu{rla&qq4_($Em|1g7}S5{=u)0fvwT(58H4_N(8IzN3KEYS2q9nu=u zz_Dl`L3Gd}3(nqf*2U*T2!4%+2dSIiO@0fDk+P9~x%M>{lX;Q{<;{rqgX!o+o5ZW( zXlh&)ChNq)!a}#%k8IC=z)(g6Lw~y0I+0ecl05rbkS+Oz+5!X$(_GPjUvP%Vbh{fH z{x%Q`_;^%(OtyP)Wi@0?F$^+pS$KFL0yQ1}O&C=(x&o2!KaG2g2uzEJ48p2YNyY;sWKNdY)P_aq(u4-O=N&xDm^yJ7^Xo zg4f30+gmR&0zSBl@C({|`NicVkK2|+V!Vl`@N(=wA~oVC!pZTQs;NM1v-etXJ{@9J z4Pc6x{ELqrVA17eQA=pEQynN*U4(OLO3e4p?_P|>`^dtj%d)wY@DU<_|#kC9o zWHU&z5`&fDk;`WW#SxpyaoZ;LRM+*siH^s{j#J>|R;#VOj_gOCazAdWR2w{{h~FOk zFJWJMU}%-n)7+GM$!DrG7*|RH_4Aec0Qcu@f~Xhru>=5QV$du4oSV3h z5z3BPP2?xWL+YHa0XCbCP>z7t!QYM>VsV5^id^AkID5eD|9c8Rd{~S5_;e3Nt^*<1 zz++!{A>cd~=8!A4d&dq#HX^U&d)G{veu&>6C;a6#Xh&OVBr_xgz3af_d? z1G~^2Ah%%9*cx7?y*)=hEhE)RCc_~L$*H5oeEdCOD>k*arRt)Be0)APCkLnNk+Pah zX)NdGG2V-x3g&s4ipt6Wz*`;}C8f{(Jk?z_)%{c6O%x7I*xca}Fk7L>Yvf47;^JZ( zU?^Wk4(2_a0N>-N{XSmITAMW;xsxFFc(CXPrH;se{pbl=!JhH2U6U*x^yC#{`nz80|TJJvcft9Bm*-1Fb_)&{3?Z z!3LfIo~vsNk_6nZ1wL`%>yi1TQ>c#H;k_JI091aI;9xfiQl`d0@fCw$XqzV=1|27f z>=XrPY+0!`J8{o1;E{J( zD-+Jgj7O>gPxkTzimX38RT@7w$9|SZtC3mJ>m3do)S4C4NPHJ#U5FaGaOzV}%$l10 zGa7p18PK>26rW*WR!iv!3kFetFJOJi&dw$r;%)P3bC|zUA@t`LRBKj>Zq2m7D|a$F zYU+nh6|cg0DuG08S&QFGpKB)__Kjv5@G^Cn2!w?A=3WH+5!}p13C67v$v|_w-)l*T zBPZ3#ZE3!qu<&g$)nu^a%9W<8<7_*jpZB`o#qQ=y-oN4Xt|(dF}vfI>^( zX@+4QFpV0-N|*3tWzG2&Evp?aR;Lb+Co2W?KB)(8oCmv}1AU17g_BJl=#^hRX=^&a z#7mBW`g8aj?O^PD?0E6Gn+P!~0ir&!U?vw)Ki%f#LvQ!b_qzF3u1MRGoNG)*3J|pH z=Euf{+_}gjKo7GHFn%oU42C|z?FDI$LKg03N8=2OqRm|jersqtk2;C2q}yr#;`RT`KmC(8o$A`hrpZ+4r zDuX(|d<&K`Lc|PjN0Q=DVS_2xyVbX7D?rg8WE({y(XEpNvHoqgLJrmL;U<5R`#i0F z>*O@{d*W4mI2xD7HFdF+!{`CyMYOrE^ZUN&7SAA;t0xplI!6Q`{9u!8L!{oZ%}21u ztYrnI;=edv0;p~3{t@1LA=!`@vivJPPsZO2_29i;SDI^568xi z|F$q1`|&dDWOKQNQh?yQOWun}h?EpHx^PT6q-$ixRU{8Hprcy9TGip~PtxQu^uF^z z=4^)sQnemh?RNV5BUa$s7hs5#_=%^XC|}TrqB_HUo}k4Vd6tX*Y=#^hc&e)qpY*cef+Qx;0Cp~RqQV79!s z&+d~8v)e0DJ_nEk$aaGT%UJF{DVoQFS>^v}2I7&}qgdEm)`ni>AA^EUH#5xS!7osm ziLRdjWHUvWM(wN|n6rc0uUU+0%WaX zIE%}MGP#NW_YwqJT1I+Q?#}_imnS&#+zcQanvNB=zmm4 z?s~JC<>uv5;)Hu5J_+X>g<~QrjWdrYsTvL+=3tKqU@Zf1Lxx1ElYXbYh??1{g9E^P zO3b5Llp6G~4!wIghlv1)p#mn6i@&0w5_g15fa0SC(a@xiYPJ3!t z4bvCxKbiV&F3impm$3kBimSOIU?)Xtu;_wQfjKY`;Z7zU7&<`g+^~ z=D1p?{u;+vH^6~iWOjF%VHOnV<0GMI)$is|-v|7@|Nikw)8ueoMX3kao10($iRxLc zVgvtJJYr*I1srvPig*yIutYJ=)4$T!*}&R6`_asuJcCn@$?Buc8x#<%)g5~s&~A=% z3tkMnZ*L>+O}0iqqe=aoJ$Q+V0$ONoW{zId-)*Huf7OHCK2dozZ>TIo+x!@L`mUla zfN?;r7X;eeOs7scb{k93q#U&KQ<-bmBPQ%7MfR}=(pT&$TJO87>U0ctL60}_V$pDW zB0u(s`w69kUnu*J^~P0ZiZ_|o_vu7twGBDXMi#8PiGP5g=DGKn7x%L!2%4HLQ2a!_ ztDpibd#hh|xuU)+A&@hS4P(FFqdpgV7Wq6?dwotvE}H2ku%9-llUi>771BEomSf0G zk)Y>~658o{YgzlgI1!@LroCWI{#uNQT=f0IT+Hs;*S~t^-xY7-H7_O9UXo}qh;%q0 zhw%N$cwJ!Yl3+ngjAPOIo4IFW&D#RC*V&a7tl|O;F!aIOw{O2-fV*~MYuZXSmj%7+Sp1W<@clkYZ(36GuwM(%pSHID5z5RAgR#luZtPC^enl7fE zV6%l9Pf~G#3aDMX@V8VQ(&^tQ7$0hnTG5HPOaB!BbU89u0OC+aww7B=K!Y6ySr9;CZ3%iQU=t1Z^+$C z|MD#Q!S=Q1>RA=j=&HX?AM$>C2TEdzil+XVJOk7Ac$6Rt|55$^6WElY}q-bov?RXAJ?O4*Aeq0=?wUd-e8C=FLNw@SD8W*1)Z; zvN9H2U7T|B;rhqUMv?hT(NI?*y!fAG#D4==lV8a1Evl7T-h^=HVh>e(s6L?k<1xMy zHNN`WTmmFr)2a)D9}_(bdm)$VG1klyy{llWfIt`>3d@~q|9+ZM8wLHcnMVahSBjn` zh=~N){_`-_B+4-D`0H~PRoYauJ!IC@w4%N2wTPyTq5cZM)y|o2u(O#(+2^}{g>=={ zPpKhpyJnNf$cna!^gL0ign6dz<`Dn*Ey}_Q6a|GRSlQ5C$gKay(=tE=Af*cz>~f(_ z-tdc82%q5@F3qwkJ%HMgTGNewx7l8wSGhS07hhtzwT){-|uXBZs{rSw?#QA zkrAp#^^W?}y*AaVU-)>qC+Aot;VC+UdxGh^Sc??!`2OhgxVE_B_1juYzaWmfc5}7O zO|q>u6~d`KMauQ| z!#%XGzr`_g%{KkJDaDD0o|VSsl7c`lnK_f9Y@5g32bn5=&F8QY$%HizccGO`kQc_p z9QIi5#&7aq86|RbJ*tp_aDRvmtI4iQuO8^|oEUUuXL&qzACDfJ!Yo%^@{hiE44fDf zOOe1oGRQ_*Cj)UD3NfDY{xuRB^zU}B;efK93%xxs`p8EejDl(P9{@5z&Aw4SHa53i zEAH0!1T5_H3;Xyxi0OIB7E^1!_(9=z|wU?B@QVEmTm z5`++A6a>N2^42pizJC6~e1GTgE%kTb4?3N$C<>ua-{S^A2z}o-O$%d82ytB(V;lX1@dq3m4u5CLILQxbP=hiv< zmr5z^bb0`QXf%9Vt$KL}yB+Y5p~QDn~xC=C=vQB|ccQQ7x~=ynZ+peTy2>q?U7 z`+?)Qx~{3J%oy)>d!FZq!#ZQ!bv;QE8Dl{Zgu|gX=aoPR4a20A$+F~mp6>^`uJsRw z5Zbn5nx-HKkw~cT>1G&~aew4TKDM*7XWRDM-~P6L`)~h+ z=lLK0@W)D}@>k`U?BgHsHjQ!L55Cq_y5C0x0do#K&*%KFIRIlM2;|$@Df@mtpB)() z`qG#FVn3td+luqRHwq~B)lxVQoIsE@gx}H(XEfbVT8U)UUfupqE}%dV8ZN!}K=_y5 zo6W~WpYoYB88~v_d3(e=^UAriH+vpHf?(LT<+9+GZ4tx>Uf&0XKR#! zQ)Ajf(f-5NTbH(NO-4g;@#v6JYWYvkcRzQPf9!$ybDOkE)t#!ju~xtUcO^g z(`=;LfQ+ZGVVURFYfaNm=p?4dudZ~;ZO8E)MUFXCK3@soghpI_AIBzTcKz) ztZLBn6iHGgQ6dB*Wc$7s1dit&92iK2Lv7QN1c3vfiDJjLTBa2du*msR*I2Anjtva< z+j*l}`|{13m7UfdcZ?RV)K!=_1hf;xJPNTxc7zM`@}=Hljn0Cvw2?9ARri6$T5`7eRc$+{EtU+Rk=~ zNF)Gs^?XUtQDQ;^8k9{pmsXY+JF`RD`Af~6qA4tQXy8YZX}g8>w39689`j9;s}-Hn zlCDV6FqxeQhg6|hvCh6;4G)An9hW0yP_8K$0I+wnl^KuZ4<#g3WIm8ZI8spc>;TtN z^A~TJtrq7@4lA5-1c7b(p6OdQZEhPYHW*AtY8xF{5g`JYAcP@=uwPt2Bq2Kyg%EH6 z5Q2EjG(LwLMV~_A_<&@f}+&TfBKbIWn!sQj`YZRvZBU0B8UdiR_kJzxk)H z_&!Al0>Bw%w^!^CB7`wSxUxFm+1ntZ%-@=Y3S(Rph+{ineCEuh%Quf5nI0b<92(4p zLMrFH+3ap@7nfGIu3TR>469#a@U8YBQ515y4Cm|(kHdbxv?$ARKenmsD(9eYm(btA zW-=+xL0{J4jpK!MItd~0{eUs9D6+1rob%h~WQ<9Yq$qMfpsH#8bJE*-is^I`0B~F7 zq5oknm*$*P%5MLVu4|!?-XD-CrC&Qtsj35OqoXTnMlO?M{1hF7*|yoi$O|Re}0L@qWuCB2!XDvZy18Y+n0U& zRDIfobEYWr8}^p$jmuU-A?y(? zEbe_9d&_-%#{wa!DB{;E)at*eviwaB0Q(vZ|0``B13!r7H6<$TEwxy{zGodSZ%v{Y zIX&Mo{a8V3?HWY2f%v^m>-Z z@f|69dCA&pcq3`x1t1+J2!Op#fFL+Nq76ie!^w*`dk{d8KraB9Fi~Z+yk{lGf(Iwn zowZg-6aM7v-jTVyAjayY?z7jMvw5l5qMpx3laeB$`DH^95dbh7#^*L{$^ilZBZx6< z8P3Vk=y)bkY^#$wWqEHqB*WqOY0nEF-~?mW4-kYOedPX6ojWg!@THr}7%>)TudVg! zJ?WH_0WMYOQn3acO_HHvLNqJ}kDHQJqxQaL)1?RJf7xymT*n{r8*XtLP% z$Cd@M-Ds+U2oV@dXCMHCka#GRl;x43k*@8%y0Uy^Xz==OabdS~aB$$r@SsPSoez8m zX#)J&#~!_BYDy#6a$TEqBC0V>J~=!%nMuv&^BCct<3Ioy01;LF#}D2A+Dd+YcP|l6 zzx!BhC~1`2J&Y6xnsRQp*ljoUlxmi%0^}ZL z076j`ch|QT|M1Te0~41@yE)ztPmY$`X3ut}2MYmU-GJv*<`C#A5zF+(!g{T;H#;@> z>Qbdvtc7HedvGEjziVz{FqeFJZTFNSL)#3zZhkNivG2Dfw_5d)dac^kbg0MlGuP_L zbSj@zgi*4w8#Fp-e#Jb9@SR8Et5t8iWX0m_?uqyxUft|vy-Y~j=ypBE1R^M^j1iJ` zk{XN$jw?YwtZVi*eFRskR%Nl^yY|#d!CzkH4qz5^~Jm z%@?1TzW1GKBtZlLVd2dO2r{6Q3(74QY51mILjynX-0J$3jms}0Ebwo|sFiWvuSKfV z8ee?&EJCj%bSu);&oAhUDPsIB=rVk*806QAKCm|~@~bYkFZB4eCwVhR@aC_7<%i(w zo%79K{{}zwwdBG#xboX%4PycLhAx=jxP5M4+wBv-`5Tn}H7EFMpYWTGqu=zhDP;hB z)s3)k+Gt=5JI-?{Ka29H9I@`;Dh)4=l}eB!^4Az4$VCE)N@Zf@$_v4!TpX8 zzKfC?{)Pwa6Ak}Mk)4hY9!d!c$#DO=%` z63uF@l0hW=otY8>0q`lu7!nK_2M9sPIHsQMQUD>r05i~EPTW?2|9a<<1z7C{wH}?# z%jpP-s`$*RxzqF@1geO~QqsNSvKLAz2(Y#52QpYw$>kmU?5fo;gT4fm8}LI%HPUV+ zRYWl#J8|s5{ddVXzFgz#Q)haT0FDhS^E;*-(pB3=2oekt0E>I>y;J(qE~7g~RS4l1uC;%Bw6NWD2?lP!Ghs2J zqLqpZ8PK2`5_#9DddG*Y4Wn zZ3o8c-Ia3N+HE=nq0y8&kx@T?wG&o|Pw7}XJs6LF_TnW~6#GF01cB+&v6PgK2v5)V zM1oz@TDx#X4eLV(X5zU_QZ-h#S__+7aa|eArZ|Ik;2kK04cl3(7ClDBQgPO3ha=(e z#Ato5+UoT@N*&-Cu7e z{q;u<9{h>>@52!Klwkxs-~as8`HP!dAz41RzT56Ci-KHiK?FfA79Whqy}<9;4(0s% z&Q8a)WkJwoStM8_L>5FpprSygav4pM`~XGd+S?9*r{~+#x%^x{$0%22iDJCos5xG6 zdSZAYo6|+{#rYdcD=UC8+p&!>LCkBplwgPvCJd^AKmf2U3qf>vcpzCwI$bu9*REIg zuplO);dXP<VWbuB2k|*^vT^OAtuN;rUcLuH&(R_-d!fMQg8C66|Kx7LFa7_IA{U zf$9s3>115i5+?1%VoI}N+*nz69r_dB?*%ZW#ufn%9k{z3`Oc7>|3a1}O4;h_#z#K#PuuOb>-vH~_Ft!c>=O-t zZ=BHJ!Q|~+J~gt>Zg?Y;@MX;9PUYFf2Tn^Fn^fDmlf-I0VaniM$z zKXytPiZlmCX(bl7Jb$-C$5K+;q)xyK32~!h48_Gm1!cABT->rxkI5~jFb@5I$25}E z@M_iVr(_T5?7|+MDd^h`@91buAb7b;bWQXFwz%g^Wu&(siac?pZMsarkaLiYh?loa z${>Q&34-bTz@dS7Aqrk!w`DVjbuHI59JdgU?KGMq0JiTZb-m-b z5mn2@6IXjBKk#-M^(QV}-fq-YMHx;eZ|;=_hev$btLjv9^yTf%;}a84u52b_DZ^+R z-L?{o6T`v3E*e=X2k$#BVyGn}k)1~K zW^vc2bfsMB*tUQ%MyRjm2snTcI*y%BCZ}^*HwY+WUDMiab`R!xOvTqqSdGPGi8ShU zm5_TUr{bzwtX3D7)+)WWCP~AYOhS>mjz1Qczb}$3c6~;JX3s2}hD5LykJyRm@pLp6 z_BZNnO7yFnyVPu|a%iPtOeIyKse8;+2-dQxyMSFS?~SJAPOrOJZB_@ewQ6rH6FRrp zp3B`oLc4p; zjU`iwhff{Koo|v-9lY)6&@1a5s0*X1@S5Wig|{1>a>GLyC?R0CI-czVfB}P&DrgZ2 za0n295Ck4$fQ2IIlmerFvEP;720L1*zOXABnCs_lv-PF`;=NvpoZ!3 zDbY=(!UWi+UuwCf-6jHD5YX_!xY=^+n_b2@5mDf=PSuKJG{(6Y5_mu}NjY#GHB~^7 zOn1#&U0qP)LOdTe8a5Wun}-w*K)*OS@EHJrVBG9lnV6nShPU>bl1TVhYRYU}s%%}U zP2H1AjYdj~mDG@&A0OLUS{7ybTMDASC4DJ^&35h+9nw}%j-lvb3)NmzRyjJ@4*GO_tC`-LpK7Aq225Yya1u!3o${Qo4Imvt4d@6l3HC z?2d75G%5R(_gq%*dSkZ3^dZL4fdY1oA38{W z^8WNhR#AX^0N1vw=8_uB@4s*tI)PTwE|5H>4^b`}ptw<@rl3 z%a%y6R%zZnnY4oN&D|mg_~1ZvBpEe4aXhVBu6KB7v~5~fx3?8hq?|(tDd!@F56pyC zD(+6x7vA(XLI@ca3K8tLTUS<^iiE?dsUTl1eA-(=}<@()sE3^ zbh}a{><5ACIAKM}#bTLoxMf*KhlZ~2>|%&4*UiSFl}>jeosMeCj%nv3;q7Xz5RY!w z>ark)WvS+~ddrA~P&}l8rnS}U9-5v55Z_#1g4D+t_iTGAn>{o%cx`LvxrK#l*Q$1n z|Ni0&zxB*>#b!OBDJMopAwn@VG?Oo=qW9|ZUfVL3_R8f>hajYgqJS|$NWfUYm=MG` zKRq=yH<%xYCon>k29<6PNw|Ze$&{Xp1?SdlE2Y|0I+>EC`L*@uu3dj-{>IJHUZvA5 zw%WyZTcEUTm?45V2ITrN8;LzmBp=MQIj+lz)(c*>IsDS zvEi^0bR(KHGL#w93GVi;?o>-1W4GP2ELPBy9ug`62ME-w9qKyCc;wWv!@Ip;Z|9~4 zjCQTFS+xUz2!@n%0Jve%TuL@u-2fM4F+30z#)l#iP1>#dP=a$ap-RV%AcUgyMy)d$ z5pR@g&4xie=9z(O1d1Nx%*UcXFc=F6_h`T=pgwaA-!%hKLV8?dj5doEHIhuk;&TNm zVc7Hd;+EAZ8CpWNyFO<;HWUtt@btmR_-L35^7dA%Yy~&Y)iRUuy^U%sXK%-Jogh6H7F5BkI}pO#qX}nR z6i_~`G`oJ!^a)0kamrXKq8X0wQ}$It(1ak)c)M)saizK2VF5@D_W*__b+Zc6OD5OWDk+rp( z`$WUHw)vd8ub{;h&ihK-ocV0+^$PXb_cGA{0pJW^0ZAdj?)jX3Gunj%2$5rWnTcq( zYH|J@O4IbKCl-p%7p`|db)o&_mCkbuy_8NKn9}D8iX@;)FX(F|`HUk3F$56y-6c8W zAg+-IXF^GxESFtLfYUkU*r0M{*S@xE@3y@4y0=+&CWj*F$;qvnU%GykQ%VR57)Xam zLc_}yHx&^e3s5+5d@6NXuf6Zij4A@t0|E~u$W&GSYCPW zz1J|^kP0=C8XizFkpW7i69|)br`vNJF9-rg4-O876y23#*b2p>%jgf933|5KiqjnNM4}L3wmW->ewROSPkOA*aDQ zg6HK^!=y8I|OA$L@9<>M=paT1=8u?7Pf0{J>>a*R>k0 z_r7x|Kajk1x%ArEN`5NdYdW1hD>bY~B{Z9rhYlsG9e-uns9EavW+R*y>+2niU~E*k zH&}ci6!5?(x<=2@6KZNK;yD3^Af@8$h+eC?axy8!M|$;UacOlhg?6h!JgRN)w49C? z&ZzN%-mO}<kA3VD4f}YjL_;{OWT#>X0fZ3|@#e+K=A{}!-|MR-Opw)b(_>M)z$phY z4c|Sk&kZPhoj@Qk6(Z#hb$qJHc%kUn9+O3^U|xs`?|pD=&y&2;Rs`BZX>B+rj;4gU zygZ(kV;YVq=;4FWyJsVZqwZ{4%ti$O0fGQP7%-qn_|vbJ=5pfMRnv5-?NK*5*6g~h z*BS_+(Ug>l2*-!j2d4G$jN}F!V@NqrMYLXXueX&(wK+-SYB)NNW+6>(B0 zca3S4wnt*AJnHVX-L}mD=MaL2WgsyE*X~%hM2IFy2q6HtDngfX zk3v5PEYB;qI}AWwL_-NN9Tq-+t=_Zklj8@*QvBjZtJ*~bQ%!;ga+#;D-dHL(!YUd~ zsUyiqRMC$N$48U;xlLEb1Y;s#B8dV)5JBisE)hPQ5LPR;=`kW;>IZhu>@{1>QaPGQ z6(+|eS;7D~j<;9qwOv0S#ctr28l7UL5sF8X@mL5GMHDyyZOfd@WX`RxC&Qt9G&-D2 z&1KVTrQ*QAP}k@o0A)cS2(}!jLy^(0CuAbHtQ``E3&XYDz0FGT?y1R$qV;Sm5eip2 zy#U~$M5bezwN6tLh3R~DJd?>rB4I^|YHGF9{nOW8w_LAbT0MuJnh38}eFT9Q1VIo0 z#xzmH7>70Wj>+*4-F-S0i}e_Nert2FQmxeM-A1dlS8+@$B`Z^j_)ucVbR7ii*GjF5 z8R)t!Nivp2QP<>fNYyo6(GX<{<4H-Ih=okfdMxlnAZbEeQ;^8ikg`-Q0%uoZW}{<% z`R2MJBo4=tql(5j-9cgBW8G$BuU;#6g2#^?YTA~q2v~wo5UBoCAzt&+^9ev1ZSq?5mR+ zH~lPs}}s8CmJAxG+=sCVF7bZ|EbsZ)VG#nQd0gt96gg{6DV}tP@d}OS{l)de8w_NiWYMDW$=Wo{ifbr#u z`^=3VhAm;LW8v1mgUff!Bi1p1Tur!ut4obL&kt$ z6j$*Nok|?YD**+%gj~vH0XiOkZqaOTX)5HM9MxVdNei3pz;OYBO(O^^7$dk+aZepc zwi|8RpAUkZo<%0-1@(}$3URVAuvyY)uE zcs44W995rLFd!hsW;Y)tAH4JUiLn#^_@Rf64o{>asV6Tj7F*4vF8V>xFzlU1R~FsV zqxkYx2S5~2l|noe*ASy`a}UDF{1KXhRB?8$G!WC8_HZ z+4Mj%0Rbc!Z8hqzF0Wdy8&c%uazznE2vOJe6AB26pxkD;csLq~B$DxHBr-iXaR0%% zgCoQ1_4;cot52Q3*s9c=Zm-=nIAi0<X0Np*Qc|>dZMAak# zi6Vd~;2J@>xSmYt!vk@{cOx38Binaf6c|}T3kphT(uJ+=Uc-&LX3qqYlGv$s4L>NC zOD}D0riQZ`QgW(57|W&-H)=*Qle+WZ+ynW6#|}(S52cp3O1-)j!jeTDh2UI7tkY;( zl3$&_X4z)XHEe2o$Y+>g5s4}eC1p_;8HCxmT&XzCN;Q_r`9Q7L8wjDJbw>~}7N8zWBJ5)^NW`^v({5Bur|aDk@#O#`!~md_zwfbpA+3D* zwfZZsY>KjS{H}>8>=yD`Dz82N%M_ z_fFgiDG~%>c4$(OSgliBx?HL^+06ZO;Y3nU-QvkBer_r*sl;r#en3OIn2d?#n(O(ujA;=#kSryT-JG18OFW3;9Sv=bGDX+RXB(<+1geTkZH9fP{|2 zGVy%=<)!ZD=FQWSp#ur_+PW1_gmmC2n27@N0zQ}!ET4C6A4D@3m+H3TLI_7w@_1TY ztFgN#LLl%wNjsWm3tQdQn#Td~e0t>G<5T1DKYik2Dk@0?T-Y@K*}INBcIfaz$z9!B z_^szIByl-AuPp7=dk$!rf#dOzLLQt6zqn*mNF4(vmU4tQ3O$yq>v&#)}|YxP(n7E2|=se~E{Z5ErY zPA?f2!z%8!yQaru>7?s8+tr#A1lefhmBkg2kW-VB=T?^I@_B>9tLy8G^IR-yAptRV zFdoXrleQcd2{0=4!O_Hk9y>lTQZudLR8kV8-DaIYHl9k}JvD<6Sglm+z223rt%coE z$Fg-vJ~}+KSlsKFW)K82#*t7Y7Ec~OFu>)A777cJ$T3QVBXgN7^xS7JTt0haacO5q z5d*0A5^PS+3NuAS$}v@&t~Q(*qcC7z*iwDLI=8 zYu1p}>veir0W21)yL(o>=|#CT6i;5-uI_aGY*-KMYBrs_cjDj~54nBw0rPFgGYh#QMM!rM4Wh<0c zqIu0Wd=cc%3trEIg0ee4qr``E`m_VHa#H0%c!z_(PIhcH3}Hg~$!wT`5c`c5xB z9^JWGCldai&I08CBH)=m#)wG7vwip$mW2oD*VZgq#6R~~hI4*($N7U-8@nw}Bv2PoNJ23cl{@}M&2<7EQt*%8 zo%q4KV!8m@P`Sb2C(o^yR@OA}mX0(BfHD99I60z12+nVq30;^O3~gRnD0bQBua*C| zN7BFW$<0(InM_9oulK-|t_@4R8*Dc{pLvE&Lm{baq1saCp6NJ2ylvB-L+yb7$+_mG zO}XCluk1QQG2zgF+OWWr7n@z1kB0yQAg*CSAV&t3sf@JOr3nS~JTDTGs-<4fb10Kj-QozV4qFEI4aer3TfJT)6ngyN!Aol!3{U~VwiO&3mR?-!T-&ij3TBjnE`$h% zM4XAplE590TAm+MgG$$QeY0Z&*8`$J1cGIOAc%ZK8RrNg1W{vsn-G%BXA<%F^uvRJ z>+Nmq!~`~xhNkV-ydX6;l5DhUt@dO#J(JHRL!tFbS%n}QjxD*Aa_0J;&GBlfELhDA zB~tZG>KZW#pPU?vAT2%+sqJ*e(#ei#z2nf#a3Vr5-l$YJYBg07%I$X7wpBqSh*+-U z2Yy1=DTGH4ObfD18FyWKyW@vyy__0+@#>zA@V%20GpWRCrMg(J6^)*fj1Oj0l+rts zsdPBpHm#Cj7A@1~G@DDr6rxE)5r9Bo&t;oE=k7`E;+A!7sSW~UhC}Z>{>XiE#~4FiDBZN0Un*&E>zXP_U~otj*|m0W zr?`zh?^i$YeF;@*StbNPfa0OTz=Lz6HCXp}VEcX|uMq|9nDwIv1}>~^Vt@kQ_dTCT z7-1;L(08EGb*RT)dftB9>2ypdwWjT@-yDffN7LEu%EpD|4lUrj$6}^KyJldi?p#!_ z*35dvVw?+Nz!@U~2KXy703Za6@gU$o`o7_4gnax{+l;XfJf3Ham#XghYnxPTO&*yx z+PldiUcAv16jGdTWT&IK*%)(>-Su~F)RnN38jt5E93pz0-f~sI5cz~2f7g-Lo?qG8 zX;vx_;7%D3fh32*BPY_KoF+si%l0kNkA@Y-H~^4yjtS(P7jHD8c`Y{+R|D#MG+_PH z{?(OxKCQHRBe~ehPD2#Fdg|efw<~76pj#cMwzHF;80%E3p6mU+2qHiT9nZ?m|P9-@|+Qc)#;P{evgs|LES>Zqxf0pW3y2eqvZ1OGy~PS62-TVMIX!hN-Y{|4e8q zBOf1D59B2g!>-E!bWTaz=Xw?2aR7jjgMe`aVO+zf##M>Hy$&6Z6Fe~brPp?=yJjvX z&F18ohILucG}7%^x-86>+~svM9wM(U8;;MAq-1;U$sF4)sMmI_-KM9A=sicn3q|MR zre(RDa-P)jfr892thvB&nMXmz0L_~3^&COKvstMd7H*bo#|@@4-tdIljD%+L3MPUb zIHL*l+>LIf8_3LVyW&~9fp@n|qhmL}aOtK0^o467nLj+2 z8cAxpq_%DLfjbqE5cDhq2;Z>%#Mp9(Y^Y5LWjE6#wPmdfu^sc$7WY5nC0EMEi4~!K`er8|31Li% z9do7H91=~zhxurRa!MKCls`Rx4MSx6{^LgvA0HcaJYOIe7IN}MoYg&fw zxQ=a_wrANdSKRw%_4gi|9nX&oGC8}swb-cGt{2iYNfeHcjgBT_MWfwvx*jtX(LWXw zS-`rc-|iS2Z9DLN&Zy@HrbAyM@YI+p2L5Iv5HLgtVhlw|fPyrUO85-rL_z~pQ1oax zyu4OP%aRv(kIv2f#65QaL;@O2}tE_9hgP7ySvv$kGUHGHV>>Qeo4Ca(e^X+ooX|_E@A-vbvSy-MpG3U5@ow8|n0wloVLNhlL z&yK3pHzW*0l|-DM zM8iJ*8m(bW5Rpl{Z1q3!J=)&0QX`?w%QaEix1R+@!;a;(e)2Gf--I{lOP6))c6Krv zO2}s0#RB^Kq8j?Xaz5oADzjAV ze(qZ5v4hdO#TG$T}N6^6H9VQ52TZy;It?9jDaxI(BfqX#K)tnX~KWe|e_% zBd6myL^#BSD9FWF%9kv~_Zhr*I`qnlvD@@GXF@aZTrQjbWL|%1(HKu4MZgnT`Ti+= zGKQ%n3?+oUmVay{^xl(qq~pVg1|H{eJwjQ~w!uROW;9W}bBto({_(j^+p@PCZpU)_ za{7G>CCYg~xhkSn8E!Q^0HN#q9%Y8(0^VG&sxK}XwVp3vG#n5A|DM?)_~yZZ+`+-& zo@Hr@TUHMo2&yBZv@$01AZ21Yrm z6;MfP>x2M$~-6&c_O1S8?_06{Iw<^VL>U9;Ydi_*ot)mE=32qYDaPKE~s5hDau zBApz}PG=(XrS^Pf@7Pf4u8CY&62q8;1gX|*S6i)U5cnqd4d{5VSs*r)>$L7!63_e{^#MWJcho^Pka+VRm?r)nS?00NVV#A63% z7zdu$R|;T5k?^Uz5AJj}moF>~L`^puO^#+lqUi7dh5C{lz-JUe7{ZJ~ zKK<18{U;-vyVhSkRq=d)3D$K1Z~!E!*K97mcJ0WcN9hb*cxjD5QC5htYw)~1b2LIr z%EqR-S9dr-tJhy{-Pyb6&bwq)e(AOAXU?4EfmfJIj-Jf#T&wm?#5_BwuC<4bruSCc zLD?)Xw6v5mK9in|!qP^Y2i!5dAfT8aLB_V>RhHU?yV8Y&iN9yW7C6UH?~%KmMQEz zwC`gd``E`m_OVYi?BnZ~PC!A#uHhpLIRFp>*Yrb4S&N9SMIrhgZy?A5=9)o#P;c#; zh`?{+%F6-9$nJV}&(o8N-Sr^veah2eArmILjH3#c1=uixmzNF0p$Gxn z<53k4#Krrj^rTLf%g$#mw+V#zO^50RwLHcFe|6QowqqX~RudZj;ZyO=hFj?bmd6ec zsKW`-^jJj2mdhZ341kTgM>&t`cp@zi$3;y-<&Kw)2&J~)Gn{waJEjSZ!_%Vei*tya zcZ?{Tb?@woA&bzWuw}7&*K3>5ios^ZZ&a;QQ{jdYEbciG1*-(>CGGn$*IK!_8U? zKoC_?LL&l3rpp-Trb~Uw4VUI3c&|f60tki#!LTC8#2bt$uWwkE$9)QBa#GI?7R!Nv z_x|@k`m=xb?sv`RvsbpZKY8}-rHxI_kt~3@f_QPW*RefCK#T#(s0V!TM$JqAFUrAK zQoOcfEEElb;4R-V03ZYcLb?KDDp3R!k|DvEU@ky_0D}VZ2zLWuT4t+btZx*xaHudb z7RYIhTH|u?|7Y*L$tslFdSkrOk3R_* z5O?mKd+vFk_bHK-e#OxCHw|tzFb)N0LR_0luXl9F<0s3)=#*e!luKs9z~dqeLuPy4 z@p7pe`u*E;D5jR10X59L<^0esw|sHpwC@EI8zqY5LaCtYsu5AM>2zmLW}UdXY(@i6 z5n{rpNZHxlHIm6Km#SYmbwUGI56!+*hI7n3v}QFXn#GWF3`8MXXCg9?CV(*kOb8%> zFvd~{DG}pBN*=IKO5`(MuJT>&&AmPGxe_86IG}{8ToF-IfSN)!(n=s@HIh#Dc3wBS zWk*MsLREHw2`>-VSs{p1?e z;+Wu#x#_Oqop#6;-6aLVp+a>krT_53{=N}S)%??|g(eMn*r1qEL(BDbCj^p0a7o^9 zo!Ot|Po8LAan07F6V--IGrjS^^<1mz*^ZO~V@wsrZZr%F3L(Ro>dz~zI3A0yC>VXr)}wY5d+2;tdi?m zLEBJ*D42(QX}V(Cn8awJJEE)9u6u#Qordo>0v<|5!PQlJZPx19n$n_5W!d7PASyvZ zB7`b+XP`5cjOw-rIz&MMWu^s&*)5~*Z ztEN(A6J^+}&DyNZ+N{mm1R6H$<@QeiNN`CgmQoT*?3x$vFk?B>K3n@9Cp7>d77EV! z*tI>!{_`9UIcE|Rcv-`8sc59KWcBUIHKLB!2r$LpVU)pRk}EETss_* zNM1P*)m4~T_v>dx?BnO_8VPEy3q(`HUdf@6Cqa( zMKbAFGHy&xtt{jlOp@_F9U&y7%%$|eclY$0m1@&=!na+SRw=Ahf*=slsNNy`i0W_c zG;ZIMI+OR>V`{|-Z{L|1%BiPT+=a6B#y!cCOZKUCcI__Slf-ooMHOTb$i~RQ;mD_+ zsni`_ceo$&tPF4K)Sf+SJ$AZr=l=9_^UbAlxT8nc6g76laChS(Z$_; zTM+^#xTzSc)%B{~+&>T>?~Uf`;Xp=x%f8g9RTm@hnBW2t0#eF^NhogRbST4~47eZ| znqIi0H=5AJ>1BIMyG|h(ZP%_GH10dq9BR{qL`UZ)|G&>v|Ia(${rh*{{m{gOsTxAU z?jG~_vOBfnX*zCr0?u1roR5NO>A?v`A(z8B?Z|>lF(!Zjw^N1ghgZ#WRPRU_?55-ED)n?dsVYjUhljV*o$^2aLC=c<0!b zYQq_dMei<^CKu<3qE`JNZ#7&N>Z)p(k-QC=ot^%CeL*<&jk9;8siKCb%FA79YES<_ zO|cL|jD;E1DAns$-IAOIUQli{dlRW^2e;V8;4hzikt9zFD(5 z0ZoJXxJ?DYQ+=) z;8}s0)K!C?-w=N<12GcCGDhE?Y^Fb2TWwB1p0BK0FK+ zFo}>9(idn)w=s}WEKii1tZcKDN{A5B6&yE+l+p`jTb%CcGlttV%j0(+tIwB$AQUm3 za3K{6Czl;VHTU$HGwc41TcaugCy-0!U~0`H7*m3F_vi?Lrs9Qy^VLO^&zF|V{(}>Z z!8Ywju1T93oL%<~6_0gjg$9!VO9kt>Qd0B0)iWhSrFm6ogBBU&b^ zpctt-d~U<H*brbSn|$o_#^Gwt{#J8eE;c&8_2i`sR%(xRva1C zAZZYUkg3SN3?1u_mu#LgNjgFhLJGw@hO}+j@Z{0(byuXiQ%b%b47X`xIeops42n!D z_w*ab=N&?o2Tnh;r@!;2ZMy@;t4{R`haU6-drOCT*Rfh}N?ol6DU;M}zETMZMHGXn zfU1C|5H4gkLh6pNJkBLjDcBw#$QUct62Z`$Rtyy%7&7y<@aAnX1tWo^L~)gWIon`BD=Y_mtGa*oGZVVJhH5}84se(cP15T%6?I3i2>9vL&Mja!uHWF}&NsBrG z00N<*L}WItTS9B+gZaf}s*z4pAJtThiSK!?Wi!STLTU+}S!&4m%wqX%JFhSpIlZ{7 zM7;b|A@oJL>2&Sh_0Y*PE7ckh01U6$zI|I)=dtz0LkkPJXzVrnw%fuxV%hBpuU0M9 z5$FlQs?oCQeQ)tvPPf^UOqBq^;*CuD#M@p_D>XltR6B-|-VqJ-rk+6)ONj z2?DSKk%^Lyr19|S`MVF#F)n)2sg7i_T;l;_7~?STTut_-oTa=Y{rX6|RxI*7YY+^C zU`l~tzywqZlwyJbNhG8^I#IoDPl{ETA8=zJs;GpsRY>*s7=v@iPX>$$kjGEe{{6_} z|M=jJH@`Of&*l7$`#P@M)sybz_dT)tgLm}o-F7@)AcF>_{Sc<*bj8yF`Pbqw_Bk=HsEk?xI+T}c;?t<0a8bgc%} zRJ-oyr^-a3PSw*Q%ILLS#f668vVXj7@x+Frl19OGt3DlxNQnr5WqXOJ9*yga^TN3r zLJ&>sMqD+bN_sGA#+CGf<5j(-6UA(Qykj(3-*BocO-w*h$ZDxE+MP|sjE3V=4Du4@ zUEqc+6w$O{SKY$WYX6R{>+_35)4tseKLBtcRI2VC+`3X)UoEYxRFP;CW!S9E+N{mm ztj*d48aC@ycB@pywKXT6Gb1UjUT}ztzi0PULdbMqY}e}sjkpra>g~gcXq)ly-<@JU ze;KSn3Pe?0S#Atn)v20F-~`|D^r@ErLd)u}WdbL-Bm{=qwDCT3wHBOQaVt&U@HoYw zQosOQO2K5v(SgCpo<1D_9a(TER^5OLKtKrip`Zi^hMtrXRq4&!V^ewW@fiytWVxd5 za*2?X(ok`^$@((dt`1@)x+^}OSZW?y_TnblKVZZRI=$v)qI9Ozqyry^T z%02nxM|C0y#t9SRCYdV+R}V$oW2ts8f6LbFKR>pyv|=YBYM~MKXSKzm`*7a8GKT>9 zPabkktOmP!^v)z*CULqYLw|V6nkxo9NyQ1|i6!eb+hX%2-}fZ}X!=Mcpis`aRH2uVZ5aUEYh6yXvuASomuKp*O5@T^h^)rYLgJc&!w}7C6>y}!LD9XMq(Ve}0QZO$FSQrY)rH}xo z429GX>eB0F2MNXFbw_Tz{p#)MT)3?*J=k~kk-|(`i+9IUT2FJFMKeZxS7wkXa;~~I z+G`vby-J|4=2{hpk*04Ds?_~lOvE(2RN;#i4iX6|gb-4&^C*gt5|H2$A;~!s zLI^H}KtAJ=^H2yt(#3p!g^zT?wQU)n^HvZfNW=j3L`Z=}}G#Q~qy$xO^)9o^IO&ENQwiS?qZD%gyu zoFkwJV?;1W!4=*}gUfv}f0_%T0o6i|BFRK&M=vGytp{$?qfrE4#A9#%h4;@NpPGF3 z5hjWf0zy?x!L0>eBQ#T3i>Mptszy>vc0^R23W@sn<`#}_AcR&<6u0h90mYQyQr*s_ zqNb|UY^G=wA?eh8yXrCo84V>+M2o83W68kd#f8S|nM&taDw5LjXDW!JX6OfurQ=4! z_7Gq|=T8wP*lh&NW6_Lml^hlXg0W~a?%6JUo6~)S5QLENp=}$r%6wr(SJX`|!)E;# zqqSaij=s!oqP+0OW^LAHZPq5xuvssAzEl$W>_Qyj`Nxi6TyXIn=KO#6J${iCvSUjs z)}{+C0+(S7?IX$dp~TARGAJ(>$sr{|0EKh4&Mm3=BkP2IEAu-rATAN(5&)?bf>06+ z?MdazLDTa117{jdPlQ}j0udD}6yLf%Hk#9C3*ONsXJ?Nw+O92@f(IroKNJCzQX++- zgh>nqdedrqg8CtvSaByGa<3na1)LPNu?z84S(iAqgTuGcqs*HszSFK<}B_-{96 z2eRtShF`V09f&$}>VyZAM+T#i!Sgi)_3EG=;EhTmL zG(R*|q6D=iTfGG$fLw;-rh@q9I4 zh%mriNkt+=bmBAT-1?ctB1Q{i=p{`xY0|rn z)$0y#-HcK~3sk}wVFW@-0)Q}5F!Dll?Pz4K;1{e=r5GaskQgH_THQAax#U7}fe=WH zKuSporWjFzgoIXPphQTc0DwY{>K;$%^cP>3?MMyBl{U_w2c-!lvXGpgDSqbYS0>L@ zrk6@FQ&%ui6{=FIDO6Wz%Pci9Xl|@Fss)QG_#f?O-+Az$scN&0_1EnhpPpTa3GLxC zC%N!<_l@lB8aX^WUA3E0Beqy9%x!FV0keZp3XA~i4m&s!{m||0eQ8Pws&}A2{%Y|v zhihC&N&rcOF%nV|44g|!u>^?$TKEbEL;!0PC?XM+VgQ2W#0owTMTP<%YXvVN0?=fk zRAmgS0rSWEI}jD0c>HrWjO^HU!@j>h_T^EDJ?8bdn=1{LNUEBG+oFUERI+)eMih#L zL|pK=uJ*L+mWcU0L=sD(L~^=cS9>xfM4~;bsYbLZ3|&!$kX#5MLA1;TByb@Zmr|l2 z6y=H!7`i*nv+K5oam!lv@Y$K0w(L+MgY&DiL05%Ycs2J zx~grW44d^|3}fIdl!8kkFLPf6fH9>QQz^yfDcr2h+N@2WVY6QDnL%|Ly5f0Qwv<%G z)x4eVirQ6=1>$=%Cj311A`oCC!}D8R!KL7r^*ja)PSr=fBHm^;i!KoOR-p`TC?hKF zh*Lk51uIOObWfk5tN7V-&U!sm2p~Y30$dM18Ro z1WX8la&g)gS7JKOMkydPUkVP-J6uYF!41T*CFj6! z%=ZoQpMP!VdObX~;?9-)wQ2|eZ{CxfEBS+M+J|m$?@p2mMX`lr=c4S4gKdws$7xSm ztvf86i9EApB~-d%D=CHPQ8Q`aFs>LXB%|tWecpQ2FO!=A!D4 z9!wACG%pl8dySqneQI6ULT%b^Yuv3;tSQQgW#>(MQ**^&dd+G06K7?={Q z5CDXv6cHU4>uj~^@92&`H{V>TFhd0-5fc(g83@F=47lX2Y(xMqgif&=ivFxJRq%?9 zP*VT{Pz(S-F(MeZM!W!s3(2?)xO4;Ihr$h|MAB4=t^maV2(=)JxK5%5rUXr|hkcov z?aZ0_4j~CZ3-6E!A&TMX+2zmNH^q^p0L6xL0S)=uo>hdr%T2YnCsid165^WnsmG80 z@3+5g|KLDpDmng^v6=O?h?aicu3h(?JpK0%Jwqv;%C9XJODe%C#R>sRu&yG5B8`%3 zN6pbr)n_u0G8?Bqc3rO4fSIE2g+d6#MJu!wNdSxxMnC{j0tx|wLB&|XSR+IQj4=X) zN?!1{M;7B(Xjf#j9+Rqqgg{?jSPlJfsTgP!;yC{J)KZEC(<@bfa>c-!U9T?Z?b(gM z3q``D-AQ_4jTak?;8vU|0)zxYhU%L#GqYgV9fuMkq->W?C0}35GobS1Oetn6>&1`~ zj-*s5#u!nGF#sV50CWuq#;Qs}m(?1C^)t2mw%))YqNPEvZOKtFK$0K@=MMlOV>|n|IG%H6 zY1Y(qv57Kl)_)Vl}8QLAn-%DsD?#IN`wh+mfVifBq2DI9KI)!28j^HuzIFkU+YSCMO%*k zwZ%r^Tn*F9l9LE7OG}O1aH_H4P{7~(h8P3*OmwG|pS`thN0(8u_(M~TqQxpUdvLNz z31}Ch=>d=vi2Z}nAG|WfgmeO-5ZKji{N)3s({pw-rflmp+T(-}ERn3+;<4#w-Qy|& zl>i2i2*or!x#q-<9pY4bVv60n_Ih#Q%m-eO-CZuqN}BqAdD(h z10hk;B)W=k-Wo-U(aiRC8EeALGa&hD=!U`zrN^X3v0;Vt<-l@<4oy9t&#Qw^_h?ed7(&}q{HOx856k{ z#TX&fpViiD;T!fO+<>punMA1O@TMX=AY{AzkTCRpSj74fLE7X~;0KouQtu(`` z&4rZx8C6s8u_@;VuSR}^+`27JFoaxg>Cl8c-{B*Pq{OO%5t6%m%=KFM^sL>YLZ}q$ z3TOnY6cC080ge!b^TQ;@(9RxR3W+4jL}|ceAfywBfXRSMc408Z1>!;iAeBJeAZ>94 zW7LY45f`Yht!RvnB#o-L=}Rv}8e!%7G$jd5?PP)iLKtHO<0sC}99>+98k!RXaGqrg z=mm^hLs^@z898mkuiIrWVrb*NeIp&Y!CadY1OW?1I&xdPIv$;xe(=o1vBib;THR1p zO;MzjK0_Xp2yx^vE|5YX7pI1b?SLs5yijHh^$*`LczV@4v}!2?ae>fz#s|iLyg*c4 zUTm=SdT9Hi9x#Xb9^*C-ITA=B3?L->--jpPwXN^^&g|lb|E0ye#3H4u(_!T0E4fH=`L8^0m5HhAxqEdn}01`$dDn%$o z=(wV%%UoV7U7a3I=tkMLDaIk=2q7ui`5FC1{0<=&@DkvJNdqRP=534tkPL;4=2VYW zR*g(YUmvecI!+jbU)zcUAdpDIc(gxStOSH0#>HCEvm8b-5CY_qwf$>LTWfuzWbyRb zjm7!)fngzqqA2D3+QO+xOem(%C^}q-uCY`BLw(&YE!2BI^KRD>DsY^x5S{r<>^r5tmE?009#+VUV~^>_89wRfudd))!rA#`|rX~$iY=*~|-SJ*S2!s(500QuMkMV{*$x<_PnfUUF#@RLZ zW|hXNx>VMd7B^~5A)K)0a9x2b1|mn!)fo%E@67{eo~{4wBgNbIWP8)vGv}HFgOqYG ztJEAW1X3tCfxsX}(sI7&Wg}#)UD_2GA4tRvrBrWTF_5f#BH&_chn^2G zRdVIbq zCP*eiTh1CjlOdCgBgSRb;VOl$q!KqsOeYB)uQiw#@Td-Do6c1zW2*`|r0Calr3SfR z4cGnB@#BAW|3h7g#QC}R^O47|eWry}G_o;M>ljO~Sf#SnlmMN{RLD6X1OPtco@D>) z3;%iO+#Dr1VVXjU^Sewy=kW^wl|a+y6RX}E_ehF?3mMhPe9`}l`&X9Aq01OxL@)qC zgrJsSLRUbgU>c+|3IroS*a?-@3RM)>wrgCV#d5%xqB=(%S4Cp+pKv73`F~)iaf@t!V&MUbZ|Z(4%TC-|XC)UOZWp zTmnI=N%LnaPSx*uZO_V);?TivzZou_E~+?%F=m{-JWeeo0t}R30OB`;GY>AiwP4@( z4|y4qAu&uL-4oSJx^$}4C_2bNmDOglBcd6q)38I|H{)@}7OAFuYa0^`;P~E_o&inO zPA*KTbkmn%v;J%CZlnZ+#;$o?*OtA4hk`NBZd|5vf>0&hGaPH{0*ni@C#hz<+Mrp% z7+{EJJ0q!fGnrL1T}l+VmR&71E9-Wxh%tfh`Ed2k`i`Z`UAnrzLWccjZPq5x@M@6; zLJ14_3v7U-M3_Kr-Hx}JPSyWjK0}z|(n9^&b(<(S^f{8KXIHj;G_^icRkT;g{sfG8 zAev=2-5sswv1p0oznQ%+<2!pI`}@s?C;U(diE=SI+OBJ~A?5RioB z72hLz5c0DXKW4@XYh_I(NXVwg@7SI80ueRU-?}+e%-I{ptdUzfK6|(})~+2Gj68GB z?nx^FmvxtCB2-r~#%Nmx)g+vl6gP|-zDmwIM#E{$6`H%cO_zy>CL4mwo{aWvF>-Fh z9qWwD6oUIsH*Ve*lLBcJuhxPuooMvs(#HtOM3tyc`ZKDO=-8s0iBf_=p$L4Ws~9k{ zJOO|NLJ<{~D&c6m{(a+#Q!8#~Txt5eq2jvBuNpMYuKUl;+v$knhY->5=ML2vN4f$L zou(sLSI#2?mKR1fnlRw+Pd0=k7)v1$6Nq!kgk(ZKZ*YB~LCwUdsp0ho(OtUy$*zbs>k1*fFl?bG z08*oLwN}knD*%WR!oO~NDlhP70ikf_!2z@=uaM8)-{N+JN3ZZHd7W$kqK*aHw zqG3uAAp|b*X^}5PXwKhm2uaW5M^D!d?$6%y&>A5iIkNo_BdifxFXgk{gS)TT_3RWH zJzU~o(@cP3WP6gL)2nxURQ zu@VslyY*oCbh&$b8UPi})(5Z1O+B${CiHAyY~x&wC`3w0RBRO;R&f=Bu1!|6{c(w8 zq%A@>DvWc^Bmlzn6;gqvL_{G{qDIjn7)uH);3$w(qv^ib^66sxa8ffBx9%r9Bh|c3 zRchDj$y}QeiLx-nRUa6?&MM|TyN>Z?au5I^DFjdCdOF5-XL?2y%^<3VF+oTQ##k8m zj#ZpHvwrThQ!jmkbct`NN9HTOugiSdcdWvYAtVVQ-&pi-7%~6^0ECe5&J+GE|D{0? zw$2zKBnUz&C8dOH(rPwq6KMEW6;Uw{C48OXFkrANer`A^(f4@3B_axDArF0yF-Rm= zC(6A$(`|$C(t<@*{BmoNL@ix$EMug)qs95UqLOcT?P`r6LMDAK37{6TpsUbw{sRCc zWh;RqU@{k@JG%6Mi;vt_{J@Rbm_hP&_GkAO?>br~7y>300#OZ1gkHWMA%HB8Pp|o( z__2YmltPef>yBED$A9;+`Oh7$M>S9tSgW(XlsdWQsT86b_Co3S?8_(W`v=T-T$xfS z3YlbFJ~G*yE%*w7_Jne3p^@nyGC?eAxoG#GLMJdlDCDj_-PEubiVwe`b4#0I*@gZb z^jx1kzU)4Fw)u_&sf2+AlC~!?0O!K;xQnC{$~j9;=Kb4u#`2!BQmL+OG*dA>mmps~ zX*FDN&9G5-MMcLkos9SD6M5&zqU{CZ)}8Txe5#zU2D7X7YX;f?LEON@IgN4jrh0G*;O00O6@v@59`9F1rcmz@wiG*|Lm z;vXC_7E6#ZV~zR=B{gW|hS=ItZ!Suh-|)hojJ}+2HayvKdAPhgS7Jg?AfyzEN-MRX zyHl@t^|Tqy(+a#&D58`|)9}KAJv0>CHXJ)XSx?3&C8WT-gF{)a$|Nql+TFw|8pKJXu&>wq1{_3Z?`u4YqjkrWynDrCn*JyEVsh z6jhZkK%5l_-EjF-se5}GE3{s8+lLb?r%OXuwYv?oS@Lkp78gKTfwkeJ22{a#qu`VR zGJde<++5WuyMY~gb|{5>1$KjwQc9xWma3xl*J6ykW>8tN0og#XqTpzoF?O)iDtV0! zE0#6d`g0pgtB6DBGXMY#n85ixD|*#2lo+rVc!M3iUFr6t=T39U@#bW8v;GT`S!KuA zIFjfa5yk>wAR_c5nOv%SXyx=X*oa)l`Unz9BzV`By?wi`HWL{sk(5G&0Y9Ikj0vTY zM8@26Fxfq@aP+at>N)a%iA*G=WQ-vMl#-W!Sr{^m|1Y%V=p};EZn)uUO;e8@J6)~T zUMM3ez+uCZ<#@!Ae)*N|j#gKjFZJC- zN2FPHHm!vLpylQW7A0FZVqz%T3a*qU4mCgVBNvjTpJG21snKLOPAMMqrO z)ng<~5;w>bv-aOTTC6&PVgQUsRgRzPTT_ccp10mY(lQz2;0Ic=BA6aZ$8b(o{?pHKZ1gC|=XKek^hKIpCh{+wSeV zlT&$rtsb-|lx2tGXT*a6=^5NS^+zw@7KmM1Ka5A*+zI;%SjAt*Q5Yj?zKpI2-?oYM}DMo+D{ zIweUHZrPrjE{JmZ5gB9wm;xNY-J`*031W??hriyzrYJXa#7*quW zLQPkcn(R#8OPY93pV^U6rq}(nNimksobwNkb}bh3b^sHru0-I4!D^+NG)*@MYmPIN zYg;On*ejD7fQ4Mult`xr4Y|Fy=g9m57b0J&{?ijre)PeIEZ1uZ4PKGZ=!SA))jhK4 z78^_=G`;SdD&E~|L^RU$MaAX{0l??MG5|yXCmj{fd!9M$%1>7QTTSe3!qIm@ybd4(Gs0>-y>cjc>9 z+xJ0=NV1Kyz_S~e5GkapVAFs-eS-^yipN5V@z;wlS`#D|ibzKHJ#JTgpUZ<+WEu@- z=(sJb69u}c@z~r(Lh3V1c{SYyO6-|uXP!AyR8)HEY^~V{clDWTWp8LpDwo!z&z*qR zt&kF+1i#V(g`|M&VA8JoZp|lOM?C-l!DXZ9q`M=EMpRR&tvSr&oujE{)e~VWX%vPB zqLUM~FcjIo7^no-#m;1QsaT41M3pEtBZ}*?gocv|b#BE`Uksvc88?JJi)D;-Pn0Tz zQ0zPG%!A9-)uxn4aKQpzS+yf6ec|W^5R~mtnJEQRp+{BKRE)R^7($OTp9>*c1|EO` z5WEatjsfrmMnn(>-Pz88T-V8kDL3#a*&JPL)_2`*LrS7Z znwpx`^;ZGYaLzkB+yDOW|LBcxe4P;d{`()*b^SZoX+v9E#xV3SWZ#)z3IM(zY~4Ei z@sEGx>Z|uX^2k&3^GiSTGw=KLU;lZ-(C@qN5l!3782W!h`NEh8Lk0kh3C=nBx)lQe z7~?^}q?F$n(+2>A5V2S^nM|BJH@Uejc-5!xrS2t(P_yjDb7p190`fgS)sg4_Bov1p z%TJbjc4k;8t)lZX55gAxxW3_}x}&kIQC(?%6A(MX7;Ins)k7QkTClI*EH~Nbj@0Hh z0$l-wq#MYjiFbCH18vG$l}Uu6I(~ZAsk>aEKr!eSeC%HB+L<(okQXuq5l94vid*vu zDN)F!t|B22074QqT;7{f-?l&b?40xQCrY*_zHfKp*s^0N_`qP~+TqwpyJjfpEqnOQ zW0A7(m_M7jx$`s6)wXwQjLXq><6JS=-OCkBY$gdtHHV*Cc9$zbDnh2#z5cAaRu2jd z)|pV!Q8K&c)l_t6zuL1SaVGDWin8IN@m{^yV6#O}A$b3wxlr;q8f<$)iQCF!v!144 zKxm`MpPX$f6ab-y%R{WbephU{#vh$-5Q2wt8b&}V7z&sw9m;4- zN(|@SG0QgJ(QV{wtSv?!oo=39aRA`0SHx?Ma(vMS5ajtAL4uLxiQPR0!SLL?bKB16 zT@kqOach}1V2t8AiRqXUU_vHz+Luv}E;vpgC-c6P5(6R_2u3ZbMbacuod_wjF{)Br zwOPO=0#s@;F3>_LFjcr=OVm`s^2F>$INod6zWCDd#vAu`mTj*Yh=vzBfzT#{)-B-#2lpJ)TeqE?G^4kaF3B0t~2?tJ<1O>k6D) zan2R}tA-+14w?;@pIq@8E{|v!0}>af*U#6Rw6~}-9%Z9#vF)85 ztEGjXyuEXvi#O!5)06N5yHE>Pg^w?rd$COTCl5y2>?YmMqU<>BDA zy-U+4qXvo?$}`8yJH}#BKvR=799Q>V*U`6VHJfa1)n421G?i!yeo-i2N(2Bb;Gq}l z5#^;+0su$~v^r7l*^!NR7^$8}dC}6NO6OQ|@py?S7zuSDurm)Y=Y|pzNzGIy9$Z>Dx&f^z zmjnbzav8W`B&pR4&B;gSx<-a-tG*V6NLoiyM$)Qa5rhyAM6=@AHBT@J7zhO*=R1by zD{{eSVGsrYSffgNDmT#HGqHHqcAd?M)@J=zkGA~AfAG8}^71ea01%w#2Dc9Gy&hu_ zoFRCrp9KKK!%&aLhY#EuxOTHz_{RJiE?tdZbkunXx*;!pc)6~7_0{{||NeIs3dIk9 z`1e~&&kI%pjIram`}dFk>aTwAnP-mt&ENc!Wi<)GQoh{py7WkLS`;1Q%CXdAV|M-4*@L)K_Gg+i5ceo_1afPx@wYfGk5 zQOk0R#o9Mdnb`yyzLo5;X4%aRCrb+!rLei>y0~BwioIsIdZya5Gs6SnHhjSFGJIgw zpoMd_o*kJ+!Qn!{H!4Pv5@{4_4*&QQ<$rm);)N0bsT5kg@&1f@)sUHsDW_K5N|P;= zgR0FV8jc&JMWFsh%%KsI5{?#B@I^?3cO}} zY)6+-u=uB+E)t3i1+P^7Z5{fd1?P*$YrDG4{;cwWn{tC$t?qInoT!H1vM+@Yn%(f1 z>cP~STXncE;QxDlE{YK#G`HbTt@#+Efvk3B%}Y1Pb)&Io=bFg~U9JXxC{l`&j8Ml4 zr`Nn_HZjz$<%s*pjN^xLYp2G!Jd^h|g^b3WN7wzj%X9^hM7jb`&Dtu3fQyDF+M+nd z)Q6^P18o`;$Pa~((zQiy67ZLqTm)E6>%Xc!QDwkSFz7TIzkkJmnuQpqzRp@)x&_xy2G77 z?CLRwvTDuYvm1dQ%Bw~q`D*z1+2&P4k?wSItzOMUv_lJ)sbVe>#=wQBI!;7W6-p!^ zfnM2X1Q3!Kd2Vo9_x3utkIhcUG~Et-A*4#rn^V53&-|tTZ74Y8^8V9}-i$hsRd3lA zYx?}~g46VbN?z{qAumXQR0;}_r6xPN(QLTUY{I*Ch+aPy@j{-Fo{R`Tv}>MIse8$! z`pOP9=OG1v3oh;mfQ12EB&A5uB4~K0|6BbdM`t6S{Zf8vu6cet#1Y1jh$)$bswmjD zLP8*9yy-H>7Yi#Jw_M%U-Dy;(0$6)p8E_=}| zUI2tCSva<_HP+of-qu)mN{g0iD!Gy5+Ehg|Xv?^i5S(AAzxLY92fu$n100Q*uQ}MB z_!#}8zd1`HN_$dm>xqn9+3A*?dcna2Ua)dXDKWu9$nvr^^UV6T8@r2h4IT>BpqGGL zvb@;H4JWOV>sqx)GJ@uzv`|BcU;=tfF%ueAFu{=Qie&p^04Q*oRr2hr&$$46eplTZ z1cyA_+A}oPGi13<&cx2aEnj^4{;J*BG=SW!&3e%aLe_%IU&SzxGu4Q7Z`-fv1`k7c zB?K1$co;_0ZCzXUo_+eOU++MCIVymQ+6Rm=V@yg(>GK){#u(=uAw(&?2r2`B=LOkp z`oMub%ggzehJsS^^{_|~1er|g+H3dM>lVfk1c6}~QVPb{*Lxha4gh0J2;rOyAqXJ| z0b?QOqV@W(pTxF)JLdvpAmoz3VDvo20p|i^L!x*+8!8vOwAiiEsB81>v2q7@Wgy4(k zoH3?p%7FuWH#W-AsOh@Crm3yhw@%ch@27PpFMjdjmA!Dqk)kLUUmu2ya}IF+V{i$? zfSXO{)?2SXaA0qxQvKZL?lDahAyg=A+<*ULANatJ+?> z?c25u#A4=o8mt>EE*8$5SqMXhF&07yfksDrMn`*+$>`$ZdZAFI^qUO<+yolFrC1S@ zm~JXmUu2o4Dsk(6{!F=JOM2yWDR4O^FTETA@*U&CZ1Dh5vkl1%@}O+r`QmahgiNA8G8tdqm112+)47~S5fJ-6WaOk6t@^+P^W z@C}_ZE_DSu;`H(9W_ygz75$sH#I_{(Q?ucEJ&5as38YalRh)^?-i%hX_?pYhHlqaA zYN1LYZH+>N>nK1C}yOIr8oLcpC1p zXl`h#xb)b(qY@+}!T-U0amd(E!x87=yS(?`=C(fX=`DG&_b zd_bU(2qP5J$$CA^*TT+((iS7P?ufe{Kepu7UEbmdV0@l~^}?AsATA}BSfN5w@L;az znc0Q+9c*_3&IQ6)a*jHaXnQw$WHD@Sj{rfJp%eix^fg6ANK#cpQnJtkj4?)z%f`1S zfBXlBQbVOIj$C==@Rq^&@BHP|M%jx-Xgs0>q4a!SX#^4>+X^!Y-E+fY$v2{krs89h zwOg*ul`FyGhT}&?T30Wm%mXG$t+1%4L?YKG&{{;tC#M?QM&hG`(Wj4AuH2P6GH3aQ zVt6cEu={(>eLIuIN^p9*ZrPy_h;i8>C=m*P5LHR&GOFT8N~`(o1$5l!t$P}sdNRNB z*8af*9kWlZt(~p*?`_NU#_DS}RR9o5r?p&0ziuR&jnRh9>P?}k_|32F{_Iy)CnoFG ziWO>5UbVUhfNC_+VdjPs0N^`eqwG0V4`T?z@QR_`KYYuZ zQ_*<9nc)1hPkec;vY}FC6J^+}%|b6?pd%0N8AM1vacu4~?~&Ha(!C?ej(*M(@iK%AOru2C)KnU&JxwWUK)Axfj zXUJs9Jvs_fXYwWp_J zX=(NJ>FGow*5BW|zFw$QYC?!;G}6=4)o57x{CazPcIVD5oQs*6xz*Kmd;#+iLP{x; z$@tLFKzn<3eSKqgcA?R*T4U$#?vAmsVau{P=l%V?)oPuAvaqo1c^)CSudmNAjH#)!$z*(dd8B5Q-uVf^mMz0WLw$`#^VF%S zdflQFw@?)T?0Nn**X&QHlVAPnBTGwpUDpsol#=`Id*nUu`N6GQN5;m6jvt?hMorE) zp9ScDNvo5px$#P(Cek8a$tFUd$;lWU5PIot@)vd z>a^kUb47Pow=rEpo)>7yD_X?RNY(ML=%#l)SD!EXib}V48zq}BmxI2v(x25P>k&#? zzNa!^l3>sjh-&x;u1L8IK0iaFf!Nt?Hhs<|YP!5=F~((QN~t>B@^}!+Vk2Zi>HsPQ z02DCUl~fuYr-0sdRk}T{c%fiiVhq_Bxq3Jbsbm;?hvG@LLffNa=Yp^!3h!xc^- z+v9Y-##Za05Yh>Rsp0WnvnQ>bUiE@dPz(a0@jm19synmcx5en5KJ(G3=DV-cpI&!s zHaAq@LbkSCLW;6wMF$5|LSNB3^t@+581`l|R}Bw8HZ?stlizVouWyB(6}$}V=hcE= zTuPM!lBm!Ki_P#{F~~&dfkBfJJX7$CR_Fwx=?jFAO3ot=7mge*BwGL_M5WjZ+^9yF zIPdrePC}}v*@>@y%Z zI}p$I$7}0Nr{?=MW4<6N#uOBt7U%0iNVonXCO7~n^m(ITXZqt-$=z659^SjVw7k}$ zJOBntpyfOldMt24i4ewM6)`4&5NIZiwi%hOsGd->{fR3h`%_wi3r+x=hTaxWV}MPN zVY9wh#S|~act;3<5aPzYL+^d#l>uWI$)o40m&4AKQW8Z?_Y7f-#jE6@E4rEP8ft8; z!sQ*m=lO$!eINet&u!m67K=qyl`b!@-FM%k2M+AnzJ2WVum7Rtdg$>) zBF4FBP1-Y=)LnOdTvgRttuZn(@Uf5miR*greCJP&jt>6b@BLc6UVrC1e{yZD*z%I3 zl>Et`{EsbLM*jM*Kk+~Q$M667pZ~AxuDkjxUwKf|)Z1^rSqRA(Yc#Bne)Jzd^{LOB zrhzf`Ja1^I|HB{txjlP!YMLs9sMQ+x-uuX3{naNdtEp+K=lPjT^0$8Lm#)703RP7p zCAFIMFaPqnmrnrUoWJHZ*ZuM@|5Qguo2F@uv1+yc=}&+D6QB4@i{S95fBM_owvGJu zZ~y71KJ~d&D(U2%684I$*c_daA<&3C{1ZU6X>pZUl~{%&KV{QB45 z^83I4;lqcIudf$h``VjyUGqHO^Sn=e>hmA{=s!^kQX&BK!$153?|a`n;_){%@cD^yh;h_&@*WHx3>=P%4#UvFLC9<}XAd=5PMyAKrcUSAYKJfBda){l1_5 z*!oy>31H@Kb;O=l@VHS2Rrvg0QczYjkvwbMf@k zM=q6nEHCG0XBW0_9~&DRI(+!V_q-eacW-lwG0x>uk%)2p`03f%ysFY#&8C!Gb=9_9 zF1fN&5<-lO^b8GkEiDz6mNu@wdOO-oo%n71#^lxxt#s!LMI1{C=FV-62)=vG-@#K0v{EG)S zLMG`2zxHprRWJtdnRG*Tc#++@BR-H({_|M9Kdat27K<6UKck*lcAq@keCw5|sWs1M zg5dK{mkR-e2m>sX{IL$L;fe;*Pi=_dI8s&0gm~yo{nmVkgh`9AcaP#5V$BpAs8YW$=8ByUHW<>)D#c`5e!U7h2plTa(YcX&+`w1?elG& z1{dhqlG6&&%q7%x1Ts;jVzZ^Pc){MTWx(8OfITLM+q96Q_Joo&2*waHQL#fq!Euu` zJW+Q<(j+CDonG-<*&XQSbi7ce zBIubpM^~Wr;lThDlaL8J2vkZHf{}a${S+X?1&@kLNO5GLZCUXO^UJvW8pcv^2#u(v?yb{j#4)mQ} zU#nQbsTFtsplNvm1ENIM9kN&!5(&WYiaxmKn^@P2=sHMA6%CMNVL%lP0C6UwCe;+& zXh>DXK`7D*wPCS^6}xUR&*vB;g&dt_SX5sZ#py;uB&8dryKz87y1To(Q>44Q1RRi- zZV-?hx&|0Jq`UjQ|Mvr5V4i!qGtcaE&f34VwrQ9#Qe*_{r~doq0pw|3SIhYW>zNxz zIWb@FVj$~V{T(h7aq|%Uw!0(}oXvUERRzvdj^_qYizX3VrGC(rpWOxmX=|{0%?&>?GqbIM}xkhg2t->qmVi zFc?&&lKsOGS-27T3BA7@y^O-loTH6TMK`M&Bqb`O5_Fe^EqgA$)_4_6N%}BXK&-}u zE{PYz>?QiUN#*7pVAA>)tUM}+53eVxMvXC!O`n{n(!S|4_Pc97)=T=h93uHiV-xm? z$F3YNEMjw75Xtx~hk9F?$4tP32rd%jLu`1lD1alRAKFZp5sMj$dzqzniIm|h?-J8< z+zci*m7!s#f7`*m0+3FDgSis`4%{Vo@oYx}b|`GA_AUrOgPr*K%}XZdO)}=3!pm@$>Tv<rfrUpi@tZf)cLgu|YZ27kSjR*`=ZMWKo*`S_+}v*I z774LeuUAUlj98K|Pb2qx(^!W^t_#2JTa&&?Lc(-lqhG%`TWtUnzbY&s1ikDS1&UB4 zFk^sIoA$6`YlTl*t|ciSY-EQYe9!OBqwM*v+n_6>#Fz-(pW&^C3DKy9y&Q#|jn}fc zt>nP2K|xMlUW0yXkMZ?ZyAaTriGa$er>1lc5S-ajQ z;A!iXkCU^?=3r7;-0bLR(JgaX#1)>+Dqr!V?Bp?*D21MlfC0(jPJ&- zuK&KbBK8tLc8LoL)_a|fPfZnC^DGs`0gY42$!{D3pKuiw*)&)@WYNK)ZW}(qrxDpD zrdS}VfrZ)Gi>SBtwV6?z_6XQ(0bzJnz0G~ry6mj7Kka4HCS!I&Ko%uO%WwItGdBqFld5Kzc0V6tAuTOcc4)?uT)5hqhlJo}z-Zpc*d2l4Y$RqNn{ z?qEh|u>MswZKHv~RTDYq7uydWtAusUhDn*s+@Q$9&BKAB1V!D{KiFnd-`}wjJRkSf zaaVcL&%wMHs!5`7kx)@+NyEAWTLcP<-PZ6KHKpa0h&#cWqQP|mrT{Hft3(0{j-B-6KJm7aUMI?JEe%vRlp zPmS;cmy{zM9)jtRlNj0IZio&?h0gB)!$1?|<&J^p0D`Mv@&|zIv7vvQ6Xg{^vKuyU z@)p67DL>bSCKRRK)=3xaGMl3c3eL{YWcE(~K=8d)D%2S7EZ_Yhid${R@@|+>E^j#g zB$b6OS+#P0&sITF=1f@?xjwO*tGMTS2|wy`B{ z4D_t_Oc8jrLQQ+d0>Lib_cmDe`_0-Z#vnNMjEl{V+GBv!zExhO-=)Fh{+_s~_=9B| znTvm5C+;zPHDK^1vy3|QJ-hejoj%wkMXSjMi5YnynnEGK>@JT)$!L7M-s0mcP{Q1g z8@1F~<`ROQxCP^fkSMcn_t2AEVfLl4L~uD^UD=Yqh^JY*ilu@w2rKuszmh{o8$|wJ=UkgVU0nHv^`% z#b1=fACFXKc!;-vsl{59FTjqyTD9zSwe$GDL&QpZbLXLflVIS})qwOpKwGT*w2CWBA%-hDTJ$80`Z$??Yjq+wm!~Nd;<{z>&PDY>#aKIcLAAbd|@tj0k znmty6dvWX(v_;LtTyRNIeM9%(fC5T*@fA~a28rM?Y z{QGz8IrNAwKwQAf5sx}m<}a3{$NK`@jW7dIvz zk}07#nLXW0+gVde(yWVCx59kauPbCj}Ehkm%O}Il3_U93HaLKqfT?ovJYGj6)1(^w=1}VDP&q z6((d5RoHieA@sW!_+s|r0@xYO*j&8|SiE?g@aY*bDKq#q$J)E_2eGH;p$hhzTU~9V zmeQBE35pq?@-uuJtZh<_+0E}nz=Ryhtenp=$)0Ef9)qo(?x|l@YLD;Zuw`W+f{o7A z^WE)ZWvHJ0^jEOYv9@?tSjf1bn4NX|9)6AG9L;1DP7VKO?O5Yk=liG*HLuBss=QTE9(sOR{6R2d z>et0x;dH@M-{NhT0~Mae_&6`6PtzUcZMF6rxd8$LdW?R=m69TfT8iqbC3f3Vgm3x6 zI-Mp!rsx-~oKFkyI6nZa>PAN%knh85G-M_rYSuoHC){ZE@k&y#OO%%r=DnA-8Hc(= z&@brU!co`Enmnt;2qB>T%oqy+^WH9-){7P!!7Ps!vawvtA?BINUuuSJWOVt&xnTlg zIv)t$hr+=tBUfN(GSMa5n)M$nX6cBuO;K4NNetd7?vxvpt!SCcRou)^0kPbKr+(E} zGvZwyJYNHEDRygiaNB^+W~w8)YN>uXw)s6Q@h_4t z#jR0@799-%6nebu%#}SY_u;Vb(XJP|SxmH3~t+5aZ0oj2{|{lbCwiCJkufa#`S z6y54ih~xaNDkp>M{!qJPH%R8bwmrYGQv$qOivpy$U4#dOh+l-)6k-{9-(6);79g-9 z00EyR@Q8CB|NLr5dl98P5DivQ)MZ=Ub~QCL3IZR7zKB13?VCiWzFoeQ(9^r&54y}C z#tchLO%4tQXpiT9_SHw!pfeR%lfum1U&a?3=ij%bfd^l_juvRZ@(gd=s=qRGhI@;6 z);G$oKe6gpmqgezqtXc0$b)Lzgtb&;ZpGc=-SIM zJhR>R0g6;B9q1QXKE^zf491 z9(*R-N~dlqFISZ{fC9^(Vb%ORG^YF3FX*M_^&w2Uug#qe$$1r7^f6?!95}7 z$CF_P%+~&|^p;lXMbvb-PJlI&s&-e5p85y*`Jxx?|DA1YDT#V`Ki;rr$w#~4#n=R0 z^1ogI8r&f;v#?PY%5%MFl^_Zf>8&kT%CM9O#g6{KR_**XuYzcr?62|*mbzW|fR{_7 zuHAgp>HVKYOrW9j^W*Yz|MQsJ=ivf>9*P8H_{W&;yNJTVF0YO)1v4{A;D$p@D>8p0 zrAE`z{E5Sf)`SNZZbM|j-*Kb+i=vA#Hj>9CJ5T3T2{69t-ZJdcWXQ$E-{_{-H)*D; z!mV>`SKXaCIXt$Z^Y|n7Ah6$xGMn3JG4?Zzo=v}}_Ebr{Q@+1x>qS2er;NiBE+4C^zLQB+Le^91K{XrA>dJ!R4bZ$?vT1uP|cgH|f26b@! zMbfJ#o8$SG@%NhZ+ZHOUt(~iu3Etn-1dk1Y@tZdl-@ij3Qwh8UZ-v0&|40RyM^|UI zCJ2?f?Ei*d_FGtk4gjHpfb(zUAI(M9@(7VO_6$_;CA6*0)cLEXedmWalb zkYoZ@Jc5dj@SjJ-ORX(U@295rjO`u4i|z}EN`%$zEPiwGe`hADyqvz`7*2J$mj5*7 z>Ed0TtA2|BPS$F?*UDcQDyTw{m*(~@VQdO)BZw+n{ONj~@BT3v@mt%w%4)x`WUKb~ zW&{Gc5#OaX>{edRn{R?%0d?v0T(@;;)THE3}7ou30A6PtG5*5ZgPH-D;( zG4lduZTS3Ps4WupUj)OUzG*?ix*w*+oBL-#k)Y1)`xpN7`c9pk>({-P1k5bC&uiBYov4GT zx?Tf=U1zz+NaeC%T>WUW8|?1eI4xn_2%AKA@e_eJR`x^m6@gj+VC)#2?o~l#qgJY?i=FxC3|dRmI^ZqobpFqQV(6 ze@V=eWPYla6$rTedpNA=x?9y_AOId9PFhWcU?9^bn~aQpAhVoBl_qw0S~bw${}EN} zj27rY{51uREJZ5ZSR?~*-vn{kSO`LKW6z1ULpt!~2LLfFW%%wh>KM=|*G+euQP5g* zo9J%%{Vr7b%a`%nK7;nArZ-&pQJEz=6};wlc7ekd4tx&G79tdC8X6-CL2akDrQzw) zIh;u{Qa8tB8XCSw3-(}B6tPm^`a`kB1QcXDioDwB&9K7tKDI)bPTEbrJ|= zTk~ckj~t2qJbSr#vFGy>M_9U`HpKw06pLDD8=0l z_iv0>hzs1D>TO~Rs5J0%UurX@x;ULk9}KT|TFmL#l{$_;|68?nwJ3O+^_B6R#(?Y6 zY+D4UaxD6%$0ZX15=LTyj8us~^dKg3>THBSCT-lka2+NTQHP|Kc*%GsACpn5Lv^J0 z@PCXo+bsp!3~^2br<@UkIhlylS*TO2y_)f6Oth3h#ong$_AS#F3*I#(WnUU6K9zI*bEtno^{n5`z?LmCAL*KY&nMq#FNO4 zDXR#=bzcR?a59B@n@Dh0^Bqh5g@dEsiJ|F}kBqE7KHAq|N^esg&OXFoJ`dx~SGKkn zC@*VBpwm*!_yyW&_90Iq3Jfm!7B3;wu!;tj!U*0k(o)P^{6(j}T*q2o5q%t!CaY{6 z>PpgIEqF%p^fO#&lILo^*bcYuCzMW*mii-<7vFYyk4 zhbk{ISu8l&b_GM@om^D*o45FjY(#~7e2ph>Xqi8+m|(|*>E8wa`G!SakX>y=2C>eg zJ1Ao)L>SiOf6ukpR+7o0>5+$tM@BJ{RY6AYw-$d@KL}BcW*2V>yySHNq#YmsqWL-S zsm~xsjjxrG7G*#a6P~~r)gl?9EK8ciR8%FhbhaSc+uy<&wv z;zSGX54ikKf4*5Fi#yAG@sqtqNa7qKb3A^UK)q0_$Nt=tQd+JzVA}PLN7j+2xJFSE z#Sr?T;v`0U2yU}5l8%?PT;0m{<9ZzSVA;JC^|X>gR7@bnvg(| zm@dz&)NpqT1{1oD5}eB|PIq5Um)Aff`s^Kt8)~;=g73IK4O#e>$im@@xaTx#m#AUf z*nY)UEcU+-_b2MV*#$>9GiRfNWE5tAsRL#}sbfrWp*BB66C{cY-|-3LA=S}?d6#YH zY5AYvBJ8+c1Mv6B0xTYDS=ch^U z@6XJ1-{%CK<==J$82|7(oKHGGCkLb=*wfR6)9Vc-rJ(CHM?*%Th|xjj-8JK?i!N?! z$1g|g#$Z-xNWonc-)n$#{lky64e41r+=i(^z_7jovZx_Zzt@nan#SEr0JqL@q7mcAUoT@ z()Sk-D)?I;9oVAW%+DSs1BDqFjxj3(5$NfT<={XwUx zFDyA|PRz_Tq;q#^G`wH9SU}dqS9EW9C@}%aNzs(#b}v+4G=@od<(D zgZr?HOa>zo*+i%~Ph|g?Lgr_&dn^7lroSK-lg)JW*t_TzY;zJ>l)R>QwIAPaF8dUhtNa6#i&}j%32&-uD&ZFnC(nyrUmGslMYmhsjo;|SyPkb{-R7V>P+*;A?EfQVrx zum{vZ?k{J@-7yZD3){_`+p%|Yr1W6RdRyTUdRrf6yg3<*#RFI#;1u8=pDTMHcTMk% z6!`TMJQa~mM%Sih$9q~kT(pkGp-B3Zy&~4bdhwTTZQPWiMPMK0Y4n{;0kt-ojSomR zlv9;M86iVRM}IjgTz*@1s>eWUG%v9FFLKwqH7z|29QD>UN-+U9TgH5DzGC0C006-B z5_5q5i(34|M5vXw>0eyWJq@+E3$_<3JiaI)H~p@dTV~|u;VWPz)=0PU-H2>Axv4={ z)l@!b&eR3jq0{q^ZnlFQwF0e5NQ0FJbjvJc>VxN-m%$7zil910LG*1uQeSYVr= zn>Fy`DjlCkf9u&#D;>?$(y`2=)`q7>6mNgj*(yAzar=Qy`RRMSVM4{nbw|-jd6v*^ zd5nSdcp?pQOg7;`)xwEo-*uf&^+)h1Z>iInbH>Y{;$ zk>LW!NoYwE`^Na)WkwXY4D&?3XtUX1I&f-!_xW4|#|*&f z_V$cp#wD16UZf2(WmG0WkDLKheD(?VvvD^C9Enl7DEV@3F53zI5O@1)E!&pr$um*^ ztgiUaJp5nxtgTIG?JOL6bA65Qqy4n&wMbSUaP>7*u|fokQ(eb(2Pfy|HUt!FwSgPB zxL81|kGp}!-FX=MGHgEGB$hf)767s2&b~>O^oFzM zI|lSs_JExaILC|iZoA`EK!bDV?k+x0xVZxmg?WU9PX$hV5S^Hr>4BYf^`VF7Kj|+K zM!BQnAoQtbq-t`a{(^pY+fD3lyL?Y!LpkjA*UX`H7& zdH>p(P=Yzd4g*= zoG63m-+_t7;%J#g$}q>JraYOyoPn!Xp}6nkq~U%{{r*k`w?NktkfFEW+BaF<;VLIY zUXX8?ovqtT9Mtad@*i!u;1qHXaxOfO!f4%}IlK7Ggv16ky!V&V4Lv|>X6yuCWRL*Y zrp`O|eC`g*GnPbGEJv!B&0y0-)5nJ~5N*ey_VZ|YX_8a1I|8CBViwnMSD+;I**ZzZ zKBfUYb5glP7a^QWYR?E)B4lNcAo(f&2=Nn$Ae$z5`m{cyUWKTE< zfvru=wl`LxroH$WuwxYxCue`$_s-kkna;4Q6fLae{Y~^ETEl^J?j1(N_#*Vp7g6ce zI{tWq=OnkA9=mfpFM2Sp(=xfRfPLVO`E&3v%7C^Ou1tgKu2JUqxy5OZfNjY&=v0oT z?HexyPy69o#Qb?`EXi_P)sbu8%$N^k*bMACkUCD)hRowj*5ik9Lo>gOK0Db?>%E+I$z-1&l5!thWgv6^l;I;4xN*hAluM1iXE zR3!O)gIly8@jr_`{0aN0lZy_d5Od-ATK50|B&$Y$f5ps|51(w!9zW-wT6(KO0`sDu zqJ`o5xwJFPw?q{?(n_e4YX{u zs3;JrcI+YMV@|E5??WubZjTD}JnaiccJ-VG!0e7;j+F{pzMjpXzU#dDjO3iHxY!Al*_fCf^GV7;?3YUYeOux9KDH;MiK$}fuXZJGi3_5R zW1VS=PQJClr|1bQD^;v^zrJSutjn=h?6JpEOM6e+j)gCBp-la;U#aP+C#~ybS>Nx) z%TMTGJTIvyHn_v%Q-@;EdsR`TxbWsF%cqoV%FdXOS-dV5r;*h-t);1tbClsBp`lX` z(ZeT3gJKT*vq*ShD+RN*Ye6|}b$I0j87wbr{@yXDH_LMyikN=`j)Ba<$t^`lSmbLSnn>&t{GV3m^VA=P$S}La9n)K%h2cC*> zgj(8t+qOO;fdjWTt$smulataH`!$0md;=BB>FMOp@BCi~Tz>A+rO!df=LzC+amta9 z;4;@a5;cbn@|YNfXruK1%9o#e&Nzk7b+ER6WaaVo1SjX!zL3Lg$v$8*TKRm7LdbY+ zUE+CIsyubcVM|+At3NEb>vcQzyA1A%PfZ2tDdJ-2Vt$mtl|{>8VY(>@_vxVHoMS)) zOID8iSW=jSfi)-#0t>=#Rw=6-@7duZT?d$C0An}?yvDDi6CJl}ug?_B{nt>V=VI{( zPpF5UXt{+>Ow5PS#MRYtSuh`eGeKN-qwZGphk@H;m`0g;f$zatSQCkgtcFIF=3!fP z=UxZ~9QVM?zBw*Z_2K8@BtjQsQ&V5xhvhTp*M!%*c0a%Nf4f@1dECRsiUMh4VPW#+ z+3yXx&}toB$4T1<*wQov;sa|EB&x5+1n`Od16v~l13g1S`cSlQ8-!k{+@MCc6Pu?tJz?D-V2~E zA7=#aGX~yRt*x!*kL}c1O^h4-48;Wz5@{WHR5Ujq2Aul&fmIE(wJAsso(m~`4*~W= zmuh_HD!A(u5Hx_UAEXZ8MP6TeP$?4zi$O!d!K;|}49+i-y`Mi47N+u%j@k2#33|QH z;hcv;SwoFu0fGiV8Ue!n{snUV3L08mysxNUf~~G z)Ot_Mg?a7VwMYIwPKr{bY&xlA$<1 zQR%B5951jUZjAq}SnkBT2A-HvUI$)i+A%e>vlI4Ls#e6tFFzF>X$Z$+(NwP;27qX=?+K9~6J#d7iWrjHLP?0!4quLi zH$x@-a{2O#oi8fRk?n_oFMd zMCD8pFSO2-TNoEZ`kVPQk9t_OmYx14K(cdJtlHA^G+r`ELb^ChQdW76$1<&;{(~7Y z7C6*Avn6FnM-r8Dpx8oI#b*gmy0rV`Ak$%mFHyL=PSVs#lq1iGmR`U#m+;bbgM_Fa zK31Def8J4OA0YQiIvkjBk2UHN&(z{FZJT-}n3OxOg(9QT%TMR2c6)aSE$5dBISvWF z2$dK^-q)S@a2nK$oPLJ-Ms24_h8T$jx;7N|VfOXA>bwdmi8131;69i-lux(QZuu=Y zBLH6HP)`5B%G;l*0CQ=|kH*es4=1DbXZE*r?c1Rp3}N}B(vtVeQMOFjRN0iW5-efC zm2Hh`PG8@F7-9(RymMU|iuJnP_zz{v;*Daiq>2P?$&9q!{1wl9-k|kR{j^TfVG6!3 zvO4(LzM`$;rH%~I)TT$Fvw&m=<0lf6j{VlhEMidTPjd%3Ozh1-=4;&=J7M*BAt-^7 zoZe!)M|L17>QFM8&kM~bry@eom8>#xPisrZ;W-}s1eTdg1^M*>JJ$({id<3kJ<{0Q zV@JK5atwRY>c6vp)Y_FKy{bfUOXgUNri> zt(L)`maZmk!>oPc<(^(RgLv%dRJ7vjtQ5yigpBhCLaUbwqkyY)y=r0avp)bTb^{P` zENI^WopXHrMz?Cpv=?!`-pVQ?U2|~I>t){QdD=?M?e@Sn(A#?*k@9Y;)6Yxn=`00D z?2e9l;TCZXN40F*Z}l?7=xH+`43t!hTUetY3!jKfgXYg{;p7AbSa5RS#dknEsbVY% zf5mO$x%mDSebeO8eL z%eLxk5bT)R;={F6-%ce2{+|iIJz2R1dWeTHF%|pnu8%jhRA?JvV!{`nj~VqwBUQ@| z+6);|0{h4e4`7}89IGrtpW--z|Dg$ZeULN zd3oZl+p}p}C-*?7Oi6^o%itDYW%M%8BS!Lkl>5ut!@RI#*ZunIgHg9D)WE0=N~UGd zcq@A9^EjTT2Rz!Vx$DJ8>Xt=!d)V+!U1#S?nrxn`b-$H$wvfm&JAFBnteVWYgGBJ$ zKnR%##DqaYNqMp-Qy|*ze=zGGgExba(^b`$0aSJkA3%zZb*!9i+v=-&K04Kr(-rih7!rd0aRRYUbq7 zqCjj;?_(I`A7*1aH8B;io4exSeS+VQ+w=)O_n<>|H=FWm9r7ObHMB5NCu-LRtAi7Ze~r-ZgDEvwff{^WkPM=h^O2>aO)0Zq%iD5_S8d>mFBB{FKo}@ zj%d!@N&*%L3Amk2cfSm=`9gpx2w>BKF2Zh@M46Wg{;*CX^+%$o#!M}KH}YsH{9bJw zl|-Nl7C-~1UYu-sz~1T!^YCxe3Ldw%p5eE{3mRQ{-4aI-=FJa$b4iVw6du?D0^?ip zJsIap3A2yUKhrAL`K;iLNpP?+qZq)`sW5pj^LTvDVxI#NroAM^I!{PQIb2rNxond4 zb5XF;a)NgtVaal6q{B7@lEapPRCpl3q%PB7!kK0&aj{*O!V`sy29J+`9>s;{y7R;4 zadUssvd2^??AQPWWL-#*nsfB8s#ZQ!gvVx1)UiAhlfX4A=0gC1G zL*m~(L}X;4Zn8?W7UYxt-HWhS+yqhdy{-dPB536jM6FP5 za9tbX;#|>l(W-izKD%64NitJpQ{S+RCiZm>ET| zaqs%>;apdXU^D5B!!%2(qSCB;j2jhRh_r&GvvTMjRuKbizDoQzz2~4doymC{W3a>= z7F5@t*ol(;mOlw}rb&?tYO4%8c+6b?d{fEYS`Y*=vy&rh7^uO)TO}!{ocg{W?b%+VXwA zHGe0kx(~?|u_R?&aO4L^j)w{&_iNA7vUyFQFkS17@O?PREyhw!@ykZ7!2Mz`kEVW5 znEF3OBwQPOQ( zQ*%CW*X!ksKX5DK_Jn+`-HU(M-o)e{z#K4}*8(rc^I-pl0&Smdqy#+;7TjMOz4oJ0 zcGyki=QZlm<|%%rnZLSPrhJ~Va?g7U8qdx81w8~x7LYtddB2Xjc3Ja!{n{Ax+G8o+ zi*k@#U0GRGX})#72XJWJXFWJwkAOEvJYT@2#KzG3Fz9@NE*8a~#yw^wLZh@Ss-tEgBs=jsvU$11)*%)P7$I2JCkc{-X1 zY-nz_v$lqV>pG7cd_VXfX7|-+W50gix}}owJEtu%%I+x77=W=C2=hNrN{WiVY@N2A zb_v7DSXs62-nUiTWJ}MBl3!o$T6vu;Kb&}p-~B=KdhMZn8Yb#GYYah67@J@Re6|4x zYTcS_TB>DIJfwIa?ew_@Ex2#hhv6pnL!UtkTS{X0y;kCn(1NG!!BqgjTlHEauRa9N zlxcswxSC-Pyv#|Ei%Pc`A01_9s|JKBAScT6YOgG&zQSZruQ?w1lK?NTS`9R1I5f4+ zBjCxjV{$4R_E8n+h<_defat(LYc@s_-`dUuv7 zSLbG}^8=w#F@*kW@DWLa^wRa~HK+&kobeX?4>MR<+w&)xDH9$Z338sKn}B^a<6n91 z)JeP)1N5zSM#$JHWY&82XBLweBCiqFiUc(c{`*5N57~dw!O1dA`wII6L#l#Tpq+>o zuZ3T2`!=ZmFBcYOqgm#M;_ID`_trhlx6(~Z!*U>hG%$s%b5hVs zyMmTVgLDQ`+@d$(CzeI7@vz3Dp0yo6Jg$4MghZ`sO_B5MqH(>RVFy*hJuVzUE{)3Q zvgr^VXLt<#@g`?xcYMy%LBU!2%L6S$=Qf?Z?8Wi0zb`Dz!fGO3T_{>f)a_Ft;mg4y|5g|olt#<=^Yd(F>tVy8DNo9m5X{F= zq{{3K=bi2)H0j8)NPPvFg0HH_hI>J2oU0@H)`ekqOrtv*Ehu@EIr{exYK%cIh|LG2 zjG-O=t|zt6Z-Wy_%??!g-*3iAKk)9L?9AN{XHddip?+1?ph6IRPLyAUPbM7GPD2A1 z|MkdihxGM;upJ)f|E=~xmrgH1O%A+SVTh>lSU5N$SCcCyOsOC#@Zm#7*HxTv=lQtT zYO}{7L*FE1XM}s==ylYp%lY5v&k`Fr=a;8t%9oATySqX0WSPJ6Gp1xT5p1@DGNx!Z`M2Wi7zT(eZpY z@PszIBn^`GAF_N-8T4QK zNC(P0eh;8;Ycgeln~bYg*(hd5K!W${bN6dM`^)A8cqU2~?5nfFMRU-01JuKpn4t61 zmwoZ)EI@?O{>T#QecrFtbrx15dO7;?FNlq;da22vOq~e~M@NwmaHyx`5B%KR-eO^0 z0RaL)6zB~!@Hiz^l^L1FBv!Mcv9&b}46aN)Wc`~uZCWeB-`vvBFhHE9`tlg`irQ~# zbGo{saI+C(^z`ue@4XS=1sbxv1j!6!o}vO)Ps+f}x0J9BA3D-F4(Vc*$3MZe|NZvm zX(9*+N8rY<#SEJcdaely93|I}VR0hR#e%-#kkriH)6)d7@1C?fb#BG$0S4RdK$V^q7_8Cfa_{Qu zXnPy0`@CzLFpzj29Zt2zZ|p8D&eH(QHi4}nJ9k*U8|DNdTB0F!)jz4t5@v{tWquX;NiYnoDv)yNL=uhFv|olMNfif&qp0aEt1OL8 zj|$W2MGBdj%l1FfcW7DBt_baKYw##nLqG!2t$bCWIY)BbpS{%BK(cKmz$$5sdIM0R5P83-3<*TrJ0jDUB1l7^FzO^*IuwesNh z*25?xrv4W^xeGA6w^M?Fl_E|xug{dzx$nAJ?#rUF?(0&; zPRp==Flh51o0(dvisv?ee0nD?bK*-we%-w(^APm&sJWM8oQA>qrg{iG540GKIyDis zsehvS?mnA?7dLvD$5=d|WR>n)PC_ghGcwiRSso}(wT3uWs0>(OOZdSTXKYkm--#;a zlX}0es3Q!T@Qg|&Ek)K+X3*S{TpmEz^(ZRYALh$@s&ez(X!B{N-+AX97CP8G@9q;1 zv9Mufm)&o#LgEIm1<|hoV=1I?$aS2!@aQfe4V+mhP8#I`{-L6a6bJ9!a!kKH>+HQ9 zWIr_+ZH_(^Gs2Xf1A8ft(<+0UPJ-mqccS(~cK+XRg#_OddGs4K&z@R){U{F}@{AVw zrinN`g!qVk9(JDUYs|Y}?zm`VKY#mC&0VS~;^lcB-|i{)xJ>d+J=jLJuyDqd{%-ZM z;&wT^R^D^n*~Fu0xt@1UIG>GmuEMU`k?Y6bG8E70E8CvW1$mS*3dLS0j*teDH6P2y zd9>q5LrCisUwbFJu3^<73T-qMb?j7DNJSK1eUO)xWlxj^)`*Q~;dm4VreO!a?seE{ zcnsrZz!nKJM|Kl14xr`cy1MS*r*DyBnM9+9bOLTKk1Sf5W!{s&!-o(KWgxK^!Ku|& z&wAq_iub3XcD-2{Brq1kLG5+?r5mFx7h>V$6wjHtp%euAI?bFTlTCxfnn<&;LH(t}Vh~qM`w-mrs_4iPd&%FE?s{D)F|&=C7$| z=ZR1}M9_)tH*HagO@ipnXmj_o`OF_2iQ=iq4)sq<%fGGiea_2D^POIbRIQ2IcJBe` z1L#W9(uTmDJzcU})%!dq!)Oq`ZSlNpE4@Iqx-Q1#833$A%*Z@eCw^ zdF;PU>#%v9-~=W$kHQSkYHWeJY`{%@&@*5SnltA*@d<3yMg8{th(m*K0L7B*zK|Xj z6n9?VdwH&rF2;lL*)@irKBjVw>s(IlXa(-@rvaPcDQhXT>wTqHT|K>nej=Pe0Adz& z-)nKuYhJE9j2FHMFU-u(cYDvD6e$jr`$5W}bk*`hAv1MZi|hgZuJg{Cz-w6Qw9H?I zPyiwX{Q9r|^zH2Act{$TQQfNdVy(l2m424o;|Rndjcd1Xhr@yeE6&ihbLItb@~d#9zFvx1Qk(I`p`yM zFiYsVf50hqk;xu==hZ>N^MbHbn~jx{k``NrRqWTY)$@2i2bc(uaNz1XP&Z0Ab#_Zq zsyLP>BIq~wi++5u-vd*&Q1cQn-eka`nK^|Jm3-z|#>M#wvXN>y87s>lbHEH~otBmS z>S0mte|Z>57waBmKPdT}@~9;X!5-1`oh2i;BUIW!^|Vl3Nt#v-$+^P6p^Q{J-|CXT zmSVAc^BBm!Yv^Qw7%?-zE}pq4jIR>i!DArav@XU!rOi~$h_)FnIEFb#o9@ zi@{;l$BaR=al6w`%ik|SuonO5p!v<{EVd%J_t&S0>7_!Q-KHd&S#vdu0k%0WWhxJ& zm3I0jtI6bOCQx=pFe}MX%8Qudi$7UT( zR$jhsrb~bF;7dhNiJZ}SU1gmz^84w=QH&{-k|I1%^w#c$KOvnpq?dR8D?HjK0{kU2 zX(bRv9y+sy8954EnV+i*FB;uJ>J^wLPfhV_7aQTF$%yGkS;0T9^p$?)i*|4l%M!;F zk-6Ge#1Yx-FdF|3sJ!%Dt&bPOX$~nb?^4P(X~?Bd_rH!g{wFuS*Z8LGR~c0FRySA)4mWgTMUM|vb^PC@175pu zTISe4Yz0~~p8|ARqk`G5Y;GCMIfsmpNp zVc$^bhDbaDd=}YBg$%|XB|ANpA{smXamB$?Gu33J&W~!p`xUDE7QEEGT4ys@qP<>B z`3vS`TQS=^5$Ge$GAqFAQTKcEWGQ>^;P)E}!pM(4 zueI#f-TBe`?9u$17K?5_J=tdOz@`!G^n!{vE6XbSzwqf|KaJrbcHXcGCqwL3i4iFdCQ~)J&h^29ShBvD9vY%x zkcfCgspyg=Tkenn<0`BsB5MPwTyBR~p)I(+aah)4OaWU8q;-H-#bIP1W?-J%)cc?4 zLaJq=Efm1P{d=F6#;xzko@~jCUFoCc2fHxcvIA0~?oY2j(_Dnn)A@v>zqiMBRLRt^ozP z_M@A<{aF~%8>-E_yC#71$Sk>kx~wYdJA^@zhg79v417Y)*C z7uc~T#r$sU{M+kv^>#Jr^*Dz=0myNFUUsX|PuKsInygCm-6k9EcUv|hTptJD8@aJX z7>7Fu@VXF<5E-_M8+r!^NA{cEZ^q=n{PUhm4zsK6R7V&%O|t(DK*_>gT^lqs3aEOBpdE5&yfE)MVT^&H=&hqXfR@n@t>M@R+zdiw3j7 zYjK9fxubtU3_dPz-H6=ydr^cl2fB5paGQ>`FvE*PA=sB zRNe0Q{0Eid9*~eGl83#8^8D{M{N%tRz=EXSst*`FK@I+Iw@=;1*Bk4I*@O|{hISU8 zSc^6uAzU&Rf3Nh!JIY>5u)tTVL3LY0&6fY(Sn+#b8MPz1$I(09PimKo&$rXgbXNqt z8s!;?>1~tRo%{2=_8-FB0pzA_ixE%7x?(?CUgQdaL8vx-ev zS-nwfZf=EFm6cwaxeQCo%FU?DQqIT*OD@m-kEXATYO9O3#fz5U?(W518nnTKdvSM{ zB1MW92`)v7Ly+R`E$(i`-HQ9mci+8?jQk)22Ip}0-fPO5%ejc|*`LXtJJfjR4hO9S zR>(Ytu9%giK98|*mY~`oC3-M5sC`~al(toLqciY;g7ZmBVPcL9&NZJn zCQ~)L{CyMI+NaZ0niy`tr#hkUhs-AI$XHahdXKRRs7jT-2CW);hHtSM>5Wwe-$|}t z1R0hK#B6`{T3udeJnr8SDZL!oKPx9k0!r0zhS#~*CDM#O_Bv2Rd#c++?u~Aw&6*Kn zF{F~#O`9U7&{itS@Q4CEV zYE5ciO}M(5#GaGV?{XA!vs)5xIT)zCsg6Lfy|-)M_z7v}o0=p>Lh#qnXYDo4bYcz< z%a_Zrdjts>X0b^1f2in`=d#iL3C2GdXlo;ipt$rgp#L{}ID=n0-f*k?9R3RrWxskQ zv^<#m%K6+opPK%4Kp{F4Bz){>J zfdU+iFY(}_!=>&_Qi5^dy6<$#ADTZIn!LYVMiif)n$e2GQ!ZTks>oWo&BaEf+x3(*+e?27`G+$iGN?>ji|({ z%UdXu2gDY~tICzSKt7o7`P~jiJMe#mXzy9QmlX6n4n_JStJgDdoL_dSF=L;t@#2;m z0%vKsa5S)b-BS{F&aE7Ts2Um>YagOvAogPCbe2u>>A)-2UIZ6EWPiclxDeM5YtVC8 z*w>s#b(SyiXtuvZ^5tFEwW4H~^G2@d`Ra<2l!wtdtmn=7ZQ<>)0PN!`ezheaIP(f6 zWxbXYwP!|NxH;>38-Kf?dW{3qXN|qVIN#mT(RN1zqA#zJ#8Hbw7QJ<`#R)aSnkF|B zM(NWbfaYq};FxM?Xw#M|i+Mn=E6zCM4o zB~3dWJwEBc$O2XWDqDE0T3AC|@D+q>w!2}Zp~vi#d`O;H`*PO_v%dAg7>s{Cq4Gi7 zg0E|5YoeX^16}?nwI#Swgd-Q&jFlnJpHu$A_M)4#C25nmwWZF7v)pbXX6`A1CBGn( zvw$LF7ZPit-)mOOV;7y!apj?IA{7P&4|%T+f1g98{01Utp-T8Lu7zSYaf1~?)xDR9 zyc7O+0y8!Y`!c*%1}612x-%ZPp+P)O+6Jd&aeM&`_%zYPv8z1zn`DGOT%57k^|o1} zW4~1w5)$IxxNw0iWlu+wmF^=69s2K^?sTnnj5N$e{l=Urs{#&EXF)8Hq-fRYOp#$y z8ueWG2vW|=2cM3bC7w)#K*;ZZ#`z0Wgz~z0&5g|U0*yMob=r@%q_W0^Sp?|e=Sa*E zNv`g}pY1-RU{EFggVLNX6`%y;gpLz@T?&{?{Hq>%s#GE0NYZrMQ)U@tUEQL4oz)ls z{8J`-S6E2F#_1Z+;61szYzr}}EgCsdwsndOZ;ed%xC?G5>l6C0QsdcLyN5GB_imAL zd;5!aKBYuGzr-Y?ZNr%#uekgIM$1wb4t z*hN~Tw*Q=(pMU58J!)=*{$}sPmiDECvQme2ZAbT@3QO* z;Yh7j0Q~}$rH1tFvqj|g!kG05*#>TBq;bkBkY8ACW0Z)&k?>4}6=)jhMVb1PZl8Dv zX>|B`=|0lCkEjHtwdh`;0T?btSYQPmbxO}6h|Z9;*`TpMKoy6&Rqhx|uPhn^6Cn{D zCkm=AiEl5<-6>p|n|(sQ{54Ey4%gD5!>R8x7v*@C?e=TO;V`hu@G>+xd{n{BMfGXA z4CvIl+QNTI(BZwhHpJq4n_}3BPhYS+s47q5$qKJFiMgj0dP5$)_x1s_#Yp{;9-__y zF$K?((CUe#oNvsnGy7deBIA!!8IEBx_%$|h{NUs{MemJD>`rc|%sPshi{HIwV+0iU z47cE_eV|?58WQ{XFXD&;hK}E2ZuJKDRoizo(T&WvYn9SWe{=bNs8Q+=#&a@E&;bdB z2iRIV9iGsXfPKu_`vzD^2AQ|>_wVCK%Q5EB@*vzGk_Dx9AFhB*KW^SJF)_gY+(x>`bv^GoW4^zI8P})* zS63MO6Rxa>dJh{rVE{%_cH zFdba(nPHPJ%hx{S9&ev;P>%*6Vg5`lP4bD~1b@XHj&+@x zmnlW9_b3yr`9)xwErQ(C{Kvnsc>nvpzVQLtn5d=pd^XkaBN?Cozy{^x*S{kG zSl}P6Gki?OpO+@TeJ>9?Yc<7fYbu%@Jl>D&)^re`o$ycW2N=@ixSk=E!m=NIi_R2z z!JJimjvB^!d@3un?Hs+7B;TD61wdFC(Jn|o;qd<~5jekDIJ%Af(7pOe>zcgL#E=_R zhljyQ)1BX97)6R`qK<$B$DyKPsm^oNNaOzIpa8Fom5=~5+OY>#<+GUbZ3zb4oL$Nc z;T)C{vNjl^A5-qIt~ZY7d8?K;jfu4EaHf4#UC$cCFpHD=%+t%mJ%%wg8>xRD$Or*Q z%7pK1p4JAvsy)q9vOz#JPg`uS4~78#mhm!2DQ~cQfLPNWVo2o2ChshMlNhztGO$QKOP+;AU>3k3%sJFfE#VkJhhnP; zzIY(EYumU|?-+L0RY*=>g!kOUF%jby*q`W=W5T(LwPfBzd^2kSXkCrA-BP0d1lc5T zJ(;}DPu~~I@D$a}`YTb={bL4=Pdx^gQo%i12B++25niRk9%#8MG77&jK;3pL`JDT9$YRifo64 z;5w$hk+P)VgQ5lT^IQi0ev=|8iA_*^gkCgs=Lz0LMVcPKvW#J`F&|`_pMc&vr5V1? zjlx2(P3B;&c}YB_co_^Rkwn1SYR*9U&?}e(OCjVE2EM(Mv{#$qTevay+3aSkCU+`U zj4*|h;uvXm#TwYyFD4j&NLKBXLgg8BSP499NyACxUUqnNFrw49feEF7IBoTs*oOHT zh7n0*m8srqZrzIv-0*PJ$a~fmp@L0x0&Nz5j#}y*iZdI=d&m~}59*uMW|ZfDRXJW- zyqZ;t(b}PmOe{o*p6Cv;SHA+1NZ}&Y?gih)m{a)a0QY0F4X-;rYAQ|ga5Ka4BX`DY z{|w1`*W&9x&hZaLhJoFzmX4Ke`$)EDlbRY<#99t+>G{hdtGGWZOVWFq$a09RI(hW_ zI)e?$B8eDD!!!L{%U?sXlD;@*ZnO1Gc73UP-S}aMnAh>i$p*lEa2DwPrfCi(&NlRL zIugVqEX3T?KH?fX=q6@BLR)71a-Ht)4q5GB`PXX^2hV^cj7RZT*Y14xuajhk^#eUbiTC{HHK-1X*ah9sB(iHIIDE^|Th4yz6zWy+!65vY|HDV-M4U%&vaT@K+tJeUbmI`0ZZ|n^R!z-u%}#%9G=>Mq_b^WMDHu`3O|dTfr+o z$6sPjKkmUAz#`2M&a)-gDu}zYHrINzmR&%KuamP}%Z>rNGNCG~9NodAl?;i?MRGyr zk97g9i*0mkg1N>*8T+LEaU*iQLZ0@4vw|9HiT?)3%l|Yd zfzd7_w3%R1rn4`u!P`rd-tUM=vM)~&JxUt#^pZ8mN5$Gdz~#0sbEeN`V*Qel>e<}$ z?7hu^&d-U8juCHv4UDi}%rL(}?Q1B{*IJ|MCuvF7qb=flw}zf~UU&1wbCgP2GL8JOj5s1xG`-R-$14%uPw%( zIVI1t2iAgB@r}$B$I%1Ioee8$=qwDMlHcLRV70W2ke~kw14i{@r1Y>L|BI)%6h{;Q z>>{C~;h>Yy8MNN-agI!X&L`#zm|H0}AkCxY72d!|;Dw`>iX8Cd!o*4J_0v-A(=e}V+DY`aF2lFO6oO+tiL@_6HR1fGr2_^rcN=BVqF-Llcv6SWjPT>!IYsRpZC=VQ z%$EVbB7Iv?HYpvdOsh%P3`0l+%PI3hrZIpklU#nTT+k0jV#MM&ZsQ@qxhu-`mhI7h zZev$`gAg#~e(^*#?5~i)qiJ!nehy#D z8S_8=G9?p61cL7xL?#RlDt`(QsoQG`Mlh_`; zPgR6tQeH;lWUT2^`Ff>z;*7&>gKvs=jxzC-vmP=)1){j0((^P{_C}bl-l^#Ld;Rpx z;>uxb2nEr{7PDu%9nDLV49k2=`BD9e0iw2VVno(n))8cFwJ8(0%?*eD?8l5~%MjZO z4z*8XT;>~C#FwE)ot&>K3nyHk{QT^vQ%a?9H@6}p?(}>|HJ`ffgWI!lrT$EHHrOQb z83TrWzq!La6@k*^kIUB&I`e!ng6K|Ff7MmEh$?B|(Vh5(?wd4{xvJo#`1f12-k!=G zU#B?R4MmK|7uRx+;s^=tS_jl=XnC+$*;HC3otWa}dXR&uYTA*5UC_Cd1ac)bbQCbL zG7DK+CIf`mt!0&N?}swSHy>pLXt{-iI{l73UeYLS-hNE%&ERNX!8$J$SC>05a-ixn zXcqaLxmg#)4BoM_gUeVZe;r^`C3h_5?~tr88AKl2!Rb*o(Of_2B|8L#O)w?jz}yt3 zELrEIHC=Aa0XB_2Bp(>O&uqKwWi|*tdC-sc;7bSF5 zU8faKe;_%zqO&Zt$Mi#$AcCIfY}$KBr&sB7_4?WrjyaH)CJg=(8LB1CrMZUI(G=GT zZ&gunq7WC_Xc{5&Rk;sHrv@&NscV^e?#{KSg;Hv`pK!_}Jo^$02 zkwi!$*0iXNw|8=wFGWKv=x||3;IotC9OE%i(E%1!>lj5IeMF~>jAoeY|4{4;9m24V zXgvu7LniY-D@!8z8iWcg_(!od82m69e~Ox569Uf52S5Pt>sk9Ac9+NKPM^}|rF2LQ zf5s3Utuf~Mm_XR8Q?O*lfa9o?&5#2%6AKUOR&zxq+?%Z)GtP5PcF`Q$FTTpyVnL3# zn)?@CaFs;vaP`&(289Q%-G);^*M}+ClG7`R0=d2n-q#)(c#8qvck$nzR3&NweqT*o zFLe;qX;?j2H|PERE7Rk{{z#?l*~_YWtY$NYh&}X+As4{4di?~z1{&jf#xRMV@J_Jb zI@xcd%qUSRXQiYJ{>uRG7t6dsZ^O5Jp)(8P_$nQK#E!hmS&C(Uld=PVBt?*6wdvH7 z>Tp7RnE40xhJz(JitP2CN38;nK~sT$7@FZG7aW2)yR6wlV!U{iMeW=J17X^XpqFhD zl5)HGO%Yl8QdGgkBbZHhzR|A-9zhZ!O>6ey11g-lLZ*NN94`8a{fvM&$%cfn|vrFto3i5$DukU^hIOb-)k^JixWt@Vu#5vp6CDK@@|t-58s| zPdsLWl13G_n-2zPS1r_*(@L7hs5Vre^MMv$q+@Z#A_3A$%UtVj`UT*3Yt?LpJ*TD1)aplJWe>gD`ZAJHciF1HQBu46<@rpgQheoTQH^8A zxy7!i2mxb21o|mk|F}txcS~1uUnE<~tnJAxsl!EX`g@0d?}-R9JbGIas_tYo<}P%5 zY8$(5Q8ec8uvWaE;S&D9j0lXv#D}+8v;W^k0$V9l7t)SM`7*!m!*{OiB?$ygZ2wW! zp`Xh4HvoY;FlIf+d}tESn6ACq=}^@4AV#LdNY9hyjYg)}B=vIMLAM^u>nixB|1gt` z`32>f2ktl}`it#L(c$Cob0yVEUo0xR#`Q__8HPGFOuo2{2A0QoirP8QeYnmrfxHib zBSn$`FCRLHF$>J} ze4;PZ_16bg&j>0Ij3@vco$J?mpL)KAd+NMho$cLpW?x-iz8zC14~l=j7#pHko43xL zn81H7?cqx%Jo*^4`uw|z_p?VWzL;;Z-;;tx(bf-LH*;=nSNg(>e1jy_TUT7_P#pxUkrjiv^1&*IEa_2RBPOhv;o51R9L(wdQ^p)p_$Y9wX_7E#PLQU-j zf`v(Nn99;JzH_zrC;_5GzsF_0lOblzWac7?KY*dxSLKKj`&P+Yz^mxP~d#^2CF^ni+WeIbZuSIj%-rLmCOI{Fu4y5 zZuIJji`mOc(rFLLyT_<@IBjTxGUPY38yJ>*J1aZlCsD(&4yaX91rWqs5HZqsiqC_N z?GTr3csm?FOVWN;)6a|=1<)6 zZ|r|WF=F~DCE3a~morT%X{5*eAc6?Dg@FumI_DM+4d4i{t;6AFhGP+){+kf>PdR6P ziC_W^GOjSDk>7ty9OPo4$@1@@79B3&^7G}J@9M*&saVGeAqBS%DnbaHg?J)7Y7s0G zjy^C_i%Ds52;8^ETlzNZ=!mOM^9Xc27JwB~o`^-<#G?nUF*$mk6ehM1A)5CkaczUN zT*?bf^8poQ-xj400_!QGn*%`d5!&XaWpZFfe+>>~gWTv7@_yt=mexmi`W4)!rk zE4|6iQ>sW|%te0SvN^YK04^)C)M7+AHhn2K0A08UqGl2{hOdNWBXLbcNJf`a%dc}w zpRTqbCd{Iy8U_I?&dkagAazXn0O`ZHb&cIQa*8$d0mic8H0o1WZHTBDW%C*ll>4M$ zF;T6R7M74O=5PoCoN4wfg?cGc38Venxk)ZpD#D6I(-|n>M7r?pxdb<>F*mN>HdZ65 z*<5vRp#8p$t|&fVcR}s>K4~HEnJF-!)cZqmN$k2KOBYV#_x|5b$4McXqM1435ZG&M zTyr&=sx==If*Wl+Tho$+8ipE`FSTMpWT2n9(TJ+KZ&OgOFHa9HmHAecp%jq>xS+ss zsO(kHt&3vs*3>LFePsgK+R z393Y-Dh%zm%rNGf3+)VD^alo}3d(e=+^5?7*J^C9-_yK=pMU32SkHfL3Ak7sI-)R7 zv`Q()`@4s^lG&5O`bqEF;CGH+9t>RQ7*@9};%rx>xbPech|Lk2(wz>1^-d0Iy>2sM zt#Km9qMhYW_q{E>ZBDI+2dm46MMreopXAVB6ZCL!!|8!ExMm*|WOd~Y3%k8;4W1&F zpq-o`8ptBKKM8}QfFM7y?Seftlj9*4p{O6j)w)`E*;MRHtFcFz>p+1BU@OAseD9CG zGI@^oHltQ^?t;s{ZvAG31OMy;!$u)^c$N=+@&XaGG#C%P)paP+To!_R0($jt+czu+ z)B^aGi|~jQ}8cMKNo_Uixjw&0O zZ{LX;49Eurf1r*|h@r~jMJ<{ZHphHc`vCV<0iG0uG(asvsY7l^g(q+YMApHop1(Kb zRLwW1PR=zRAt9BgM?xzEgb9vLBhW}O?voZGs{c-oXL~3jr^4+OoW#b=ql^yMH+PnQ zj4GNtd>9<(xGWu@P!%xQeaB-yJXJhHz;Q ziItIZZ8&{L)aI_7_#EAGHnH0S&9?W@!kaK{`(O+$&3$-QcI(;I%=< zwZ_;4a%WGxbBY9R<$knpHach6HECgb{o`Yn54bM&8MQDjWn6eJ8K3R?(tf*TVGaeH z95e(yeK9ID^6IblAlXc%T*21r!n2wWCmb>wh^dJblC~sc0MzhJ$mJZfF=;oO4#*O(~uk zFe65n=~t#0{46)JFkmQ=&J`i0&};V1Dknbd_I2&j=w<#=_U=#nG1AfeW+V|h!mSy1 z_Sk}lN|B6Uzg!+dNJhHXRl*JQ^4{NTfNGU;W6oOMgY?(IS{B_y=6&JsU+&@E^YDo- zC??WS_Oru25kTK+ zn93S{FGexkf!srB`*+AuJwt=R-3#SbnyhAN1^gRuLUx{YdubSbbbRh_b-bXs? zI}#kEx1oJu!}6F^>!`uxCsIIJ|Dcj5P^f6X;znvHQFm77->9bJM7#x{ytu)XfB()? ze=Hpg)AtXL?o-2TYj^qYVob7N4|qO_c8JV^%_4wSjaJQxKU+B4NFw*k!%~X1vF^&M zlhaZXHR&8y?vJX})T1w=(@~s?VQ$jl;_3*}@&|+_U3s4f&@B(D`A^`&yNPlNhmJDY zq>;m1a;j7y<6<2LbI9BEb<;xs;;tIL{uXwe&$EK{_Xr=5IhR0Ki<-yl_Sp_Qi5;gf z|Mz*HM6hLnodTPzttgEGdS@Fcwp6kE`xTizE#pTFlIu9dezYwa57`tu<3 z)d_i;VV~V;$i;{rdCK!=0%KvDvo<^{{%CV*BHZgYm zNTMy>67KZ*F7NU|xSeog@c3tg?=4_-cYYk+GR?GrsYn|e(*0R1gup|95(@#*SCn9Y zjN8VoW5sSXmuwg@5g1L2)r_s*51R0h!60ypdyI40^nv=j*I6t2IhHG} z``{Sxisy^tRJTI8kdR3gWZ{X!MLVzie4&NV3WVo!l&)vl5NmQA9oR zp=)+M4~F^E$vwz*(hRx_2P z;rcHCwjplJob%X0=F3$`A#|h75`G3m=Jl!Gbyqk0#$%G#ojq;q;@6{TOz?TFdq{CL z-tvPrHw{+=!|=fXdc(rFXsSKFgUo5HlJN99&*71dNl%)lEd7fR1`eV2Xd7d#zcj zo^ahb0pW1zWHU&)M-Ao>G(r&<%{G9AU>HY|s;O$prvs<0v0{kj(y}Lg#C%=5N@|)H~R5N$v9pr1ix}c_d z&UzDrzSnL{vS%E8k^&26E)GdpUw`;!1SJ31v%(L&@IUr4*PgO<1+u$R#mZ!T|(;zf#q##c*}p zrpa(Ll!bo<)7~P`AzTB#UHs{HT+(|Pg|0~qo zc7WonNpyNof89Pe3X;G%Lf+uhJ2?`#h-t4DNoalL*(6E0v`i?O$;H7g+}6YxpA9`A zbrhmC`-Sn01M98~%&#?P7Z5l3F5iIneIgAel$d|o92vlScsA5RLjoQ7re$HmnVrrT zcTlKElZ$G!JW<81QN8~mq+ePs94Kxmlz6TJYYzfnT$bi-4Z`q%TPUJ@#}a{+Vrkf` zYB>fs@P7=tI(LINVeSof@;LmG1 zA|>bR1RHDCLHjZ{I|Xg}I8AYK6_oFPj;##R8ljzr3VPF^Y)CXduHoJMR?*)3_v*${ z>07rotY%6*`PXG7^5xI?z0x{NtPm!NV$%%wLqt;uM|W)@SE+mivceT`y9A`vE%k|R zu~}aBT1f9hdI?%ErNU%Dn~kPVb4!D6yGi&cIeir7Q*`c}UHbhIz;^HBR1@EOiWPr9 z%EiV8I!c|$u`+&By+8nnWkLBJ>B`iKyMvQ4O9MA07?csc*om(-VF~F(6GgNP>pm{? zN&vtOAO6b#PNF{j=4~yCQtN;xLk?DB#fzCY6ii5hM*=1)Ccih{7qd(-aGSPm+bWA@ z`^MWE)Q~QRP7P2Sir$X*@;HG_GgD!ZW)VhY*-%R0+^?zGvI6_jLc?+SK|z5l2VGsB z&+}m~ZA}{<_SRXwH!rYm3OVdb0uhG_Bf*Jk0W-%{G@!}wNHYkOuaC`Ordq;)gd6TI z@BXe!%yoxsrlo~d?Jr6ZDT5SH2T4UYy;_AzcSI#cM1aY5l|1n7kN@lL*!DT)bYZ3e z+TQow;DlErlEEn4XgE_$Qsjxq11lK~HzO#5@<1rI1TI{!XE}*O z$LPOdqx@f6z`}aX^OcNwc4ICd!$K<=opMh)+y7AyU~dYFQ?Mdm2cyHl)&}Kx9RkDH zMcvIR7*`GPwKC%8tPhCj>dyK!LgBE^!ExyQNQ%_Tha7~2;*svzgRxLA#zD{b$!xFN z_PMZ*jfI^P;FiL-b;`Nd=cVUR(QRypn83r&hiA%Dn_fn&bwTBUc2mwV7jqzD?!R^X zUZDJ=^0(*3Wu@^h%Rlo%N|pE0B<2e)J16C7{~&`O704CH++{al^A~9Hh+a+MNDHF@ zftyTW!9~)Iq?AT3&J=Yq@ebcFttaLJVU9_Iwm1V6N6npM`4O0*WnFpdMFXp_!Z9Bq z{`{t4$r0yqHsdiDo=u0SgXQH*+ZNm0#j*Tw1~ZJ|Y2U2Kua`*vdMbW0skA%E-s!Hh z=g$=XwCQ&T{52b_AqA_AJQ;3s#3Kd|w@d7dBk?Dvn}J*RroHnKyrYw!8L1K&4(xf| z_;>H?=myW%NJU!?a*S@2-ur~}!lSAy;M3eWM?c2uV_C01f}UqYpZq(hpF6$>{$$Ize{udo1`nh`nLe!BMgiN!Y8l>9ht@a->E5VKRdY{_Mbs)WDK zu0<8+p9Ti3v7h}`x^(bvdam26b-dF=EVIMjUe>Qm<72p5r5EwzUSBPs%dkwZxZVLQ zi6pi&CshJSleFT%@3=z6qOTV(QUkPNs01`J+k6Js6`tJ7&XP^~3WNOgX*M9vv*hAz zbmqj6Xr|NG%UDy_O~ub2UdKb^gmtr+TL^+~)f&zDxM5VUWx;_af>RA>{uA~fe@t@S z`8)JPT}C{xE7n$y*K2^MIKfsNUob81m<4PV(B7U8vy+byr5)#FCGmc%D;~2vHlF^! zjlMqeKR7!Y3C(~_44HFWhk2IC`_v!dFL3lU;b}4 zslvTk#+NezP-i^}+e!&^Mt4JhRpgFSn@z^SQt5-1<{b%-w|X4WqK+iL_&b*2I~;w8 z;EB-OIf<^|ZnjpuJ}}%~EvMq=Ov`hZBUIcB@~6GKxYZp?p2)6voA+-EeCWA-yD{6m zZ*OZEVdk@79A0tIwo{1fBe){PZ7{V#C;-r*tW8^r@v6Thm!S8NNbgI*DaW0i@O~d{ zL3Dilz6S9*Qi8cG&*RKTe`2up!01*`?IM8RWKDz|9tcC$R7+f+f1jL2(pwSqbw;w&?ggTz_ z9p$DEqeP}xMZlm2wd)7-6OtO^TqpJvphjD(G8`HX!d={AcJGw$!t^HX2sOPIqmL^1 z=`5<;D?B>{e1vg14(nIC_RulHM&lL=I?v5|eZo`KGSX34$^oU4#z~=u3#AXyvmgIC zS)I>yYS~;#icEzbXQxqoQ-9psIhL~h)p#M7eLIFa*Xy8lUtn#p0uuu2mYWk<(Pd_Z z8NwU@7Jog5S%M9DPUvXx6&&uLP zENr)J-?b=>MEyJPNYIL;NK!%y(pUj-dFz0suaY94ofYYmB03Gdg~FwV*CTTT#)={; z5S<45^(G ztg5XEkaF|;`3-Gja*hj+t$$Q3#CD2Fp>X{?7?pVqQ%v8MzMNt^#SL%-noS+X+hYvm zNme}}bf~$?LG4jO%9+lIqjrwB{Fy3BQdhSz&iX{nsFt!s+K#^L``j?yHQdlQ{8rV0 zvEl+H_{a!lt#*|&_y2n8%6d3Ly;gr>6n(CrDQ)?;dnh-T(vo=re7^p+@pgRP(sfpd zhw`0L;gxAGjLKrkN{SY4cQ0XnqEC+q zQ*u%Ep#T?I1Xc!mX5_5jTl-9)9M=wxURLN1*)0e1q>!djkio=ce5V8#W=gMNgUD>| z`jX^DT$-x0&2&5KK0<~&Bhx>eIkhC+jyZ?Q zL(ObInP#gf4yh_`zO)D}+l>N z2m~@yLlZC2Hth`OI@epi($Ns7z|J|(gUCIe->l0vItLSLhuPx%q^Xliho0+UruCu$ z(wT!SD9c(mztoeO*tKQ3=$3YX481~4$~>$balX$^a!z7|mCrjIJ6eLZ<12WZ3uu&o_3b%z~=Uy^Jx%)k* z@(7cd0hpwgU-VIDdU4KPTO7ty==bS#N@*bG7=gIqKGK6}O8!$n(PSybwS}9G^h9<= znCVkd8ppFjX>pS(qRw+F*M$j(WD4I|lM8C1yrY*nyq`|@t(9$^r=!q@odxA z`dp2^K*K1sfG!G%R`2+^CYy^HO4Yq5BoeJ}NPY7RMWAvHf-mv+iIFzys`D59j7Fw zNp7Y{N4nqPN0}Kh|F#VB~^A^ky?7 zLW5A+sDzy|=n0N+B5j10cTrAD=vZY*It}8Tb5uY4} z(?wC)Qn+8=XQfRB$27XybS^Dkd?5T`Bm3{<&^1G2V&+VVGBU}+_;+bCX z%YBoz^XuPq(_h^hGf!y~l;dOEJ_qq$tUG1f4?bCmXW!LPD9q8uwiKB#yjBlLm9zc% z*`()Ec_4#n6ZmreCpVJcoIfI1)eeJlsJ&w8gQpa-^z=OWV%$T)C_nj2>L+{S)}B%T zm$$jlTBF)qN?zQy{C#|e98hPyP#cf05)x%QNviK!EDHe|1E}Fus3-UXIw>7KYU2T{ zjQOu81oj(Nd}plxwwZYr%r*$`E=YIb2!MJze~`BwzVxLK>>o5)5ytOdo*Mrulv)Km ztz758()>!wV-X5}p0OyFWQmOB*IT*x1lAe5;?pRw@L4|l$R5Mfd`K6dTuRYpN8wi; zCqmHhAJfXezdp~xsR*#g(c+@hB@oY!EZTU(KAsd(u6J``(q2f1w^^do-sRV#>k1q62#<+{nk1F9 z-q`q)qROAsd=~2~RHlF}Q6?K1c+`2MdMtReQWOzzt=q7vIG#BNhZGzJ))S!^O~;bV z9)7@C+rfu1;55k1{u&U(CCRmfXq)RJc0@Qd20L;mvbfHG^)kPIW=MztTvR1_U+b@3 zBHMi4e^b820{Qq}bGH9WetzBC_Fl6jd33jClDHb>1+&26HnA7gocX2}$9Z~e5BP$Yix0a7q}99zIW9dO~|7E#qS*Z9FgA_+Azd z2p4$4iu4|UqQD~A)c$V{@tx#l_ZRgUnSxHgzjH79#Y)OoU&4-e%z(cF;D@xz0MvPZ z7tMCB0<0qUd4|^0O2#S2(}>kP2dpJD%Gal-BEC}7GVCaoxh;JRMt6+)kzOj{rmDj} zT{;W&GU7sH-4V%I?E>q0;u_&p6Ixia68|CH@;3P+;o~naR@`-A*QtGB=4kcXrq0d%P*Pc3gJ%FN``;BK6-Wi(lVBQ*7WCbp(#X>H5mZYF?`v<*^b#vIa zynN>MSi@nUSSOot1dd2YoL?mxs|EaCn5kH0|7vrOlegdsMz1)N)*vUPpcrcXO5IUCe;Nn z&NvsJJp#bw^KfyfZABBu)ohub#tZwHe?OwjxlCR-uQeHBzf1XV;oJ0o0AcO3GLT9kBGR&E zERT>GrPO~(K5j1rZbya(D& zI!_Nf*G^}NlDXJLeZ60{>3omTnj*u4-cX8N|F8T4gB6vBA!+R)h{@AxNHbxP^Jhn|p zofKd+M%X{Jdm+ly!COI_u)}k@bGOoS^b}HTOH|zrW)0SUR{W0uEpKwzlxMDl!C=p^ zOS{d3b*Rs`B6@^iREwWupc~l@IX8-6B!q3oHf#6mxh1(qQh|V!c$@RJoCDS45zEWC z!ju6w06&!EJ-pOW=kl~=hq-(}3qBOm_W%S$-hA#_SW~0#pFeETIv<*8sC~b!LuqGH z5iz06$Nr^nCpHYBIX`n8X@nirZO;&{u`*?5Z~LlZ@M zearn6AOHXlD*OFb__;TfthgS^E?}?6mu7qhn9=W;NCcgRj zugv`>#SOsCS?wkqk>vmnD|Hsi)Mn&0e;UPp2(4mo#ZjeqXVF<}# zNJ*u;ySr0Lx*LY>5-E|8?(Xi+`+k?-!&~r>c@?S=ox)1>9O2Dn@Of^*i;FrtO zNq=mL71JFT`}IQ9NT9R+Kvc@*{jiU;bPpF;8m>9y8U#HQo9U` z;rc?eRf0}y-;)J$MhT4)u&HHo3dW#}AR_il`|j`Rp7bD5iVD%ocQnQ|*1wIhGIY5j zxq7M6{RncqzE+Rj0;=Pu1J=i3GM}5UA6iJ+pXny6?y~fs(x^y$TBXm|gZ&~-tFx6NKDP@aFtyuEI`I;HcKMR4r##$LS|P`pHYesj z{>4#(&_KIUj1lg6K|U-r15lvg6|6z!jI6nc%G?5#T15Gpcx^S)If!07sA0LIo+Xtn z0pj}5_hKAr?KJ$AUu*Hs*5_Y?3bVn{)9LKC$33vHPU$v`{=TBLS6PYnN;O_hGY@?Q zqVaE7UTb)k=Qv&M+m8ZP)?Z_h*ES!*o|a$ZkTnAwL4}>9`G;w*$W+@4-)9FG$n$A` z{L5vg>3D9Y=$jR(r{6StBI!VE-f&Iic4C%8l1wj)tPLsCe^da#daX&72Kt*Zf6_to z?7F>25`sd-&=h|(_Q7qK18}kBGnN{;1*-P}jL$No-9hRQkoI?g1%2So<^y4bDhBFG zC%l|@1!Df2eiha7r1$7JS7PGE`j3aYPuKc3}*MihBnJ6qe9I*~L$mid%r~hGlXpBP|zC zy@Lx#qznF%c5=v_^-0|YvDa57=lbn@yOsM~B4U`zE^_IolA8j@s4=K+!Kjh}+w_Rf z4%5n0n8>}4&xu9WOBCzl-HZ2H!I|&{Tt05E{N}e|`!U1FZZDQP17cvL6Cq&M$9;6A z&a6mCv)Im|R0?Q!F!5wOAWJtRBr%7}X<$rE<<*LPalX|264Ak?WO8{c8fBh}&S(Us5|M!dc#(sn4az+A^sNKRXZtKzl)#D4TM;x>{}exw0MpqB z3vGv$obqmsc@cMEq-HRxtL-E4))On~pqOu1e}=?6NS16o$e+EUM2wZB$M`mkfi4kR zG6vOmSS66rx#B`ED4hv-j^2e)gr-i8?-hKZ$yPpng^h?|s>SsqdLC^d{Aa2PMCVAf zh07L;-hcx-<5nd}or*G-D}}muz}{`qR@6m)a>rV}nVF(f5)hh_y78ABI?zPRyV;@S z3*)YmH%{YC-3^xJp^soS_4G9*0o0$uE*;mSI;5lw0jA8HxKa{O3MjZk`U>KtrHs9D zU8DQH*fp62IXE-H?}u2_@c|&=goW27Q377}RQ5>f<+%s7W$`aL3+$g#lEfB6CBqQx z5J^tPo=@KOi@3&{5ks%4+*(Z%*klqYtwWv2K4D*GEgQT%-1Kr_?fZ~5p+GSFM{feQ z>m%8>AG#`^_eJiqUM_A#u1fT10=bArLWD0ffiTR@r~OLraVH@eEdvR3mPa9-3{~@| ztQoJag(lcS3uWEuyXT=C5(S~=DgmWJ3|~Y8sCId!LK50~S|L$tMTkOny67DuHwxfT z8zS&m9z3?{jtg<=v*$j0gggy}7sbvBsH)4_y(SL_HVAcXpOs@sxBAK_RlrBG^rY6n zSW|CHU%{&06J;GEHnN(rQ5|0Ne3`PSGTrH8VQJAA_NF(eADtSqQKgB7uvFXCK{ln#o7^J;4fL^=^EWE-FRsWh1Ysd9^LeLv|`)csdFx!oO?h1w6z zFi*OC0G9D3DltqGe_TP`59sg%m;h*j}kA~yy z4(w0@p?~R(c=vzCHkBwpxST|t9B%oU2GhqyS-Lq;SSP8>oYd_Z6{kOeN0z)1qd09{Ncw#Wj($#qDmzoKU>asvP^!E zo}ZF|zrzWq-SRtznW*mTpJX(V}r&97|74mz3ev9al*P7p!BMPu%Rs zZ*LlQx&DeJ6uPI8DE(gXZIpSGr#?$74C^7dyo+aJ&f!YkB)zHE_dj-t_T=BDe`bD% z8XDFYlQi9nsk2u%&lO6D5Q7BR`M63q!7Tpwegdy3t3y`uJ^G0Em!stY$d)=MHWMey z3HRI8f7j+v@*oD}xRlr`sL1)0d#DErrm|bvlhA}8(Her_9J1gJs8;Lnkt5e!Q~?-L3=SmaknLS|%DzXBJR!b(p)E-G5!p@r<10!aZ* z<+3^Z?lHm%zEAZ{w_zeLay~6b+g)9B3Yq>_*Wi2`aVWku3FgY*1*n3B8}FszO_GXP zP;wGcBoYX#IJdBikEIpU+b=CAa8mp`TOR^4Md&v+RcB9XwFXTcAIA0}fzP3o`P%2L zC{${Fm)b_=mp{0q#2h7;Wa)f@AQN2|i7J~FOd_CA*e?3WL>m4``DMcwksf!dw($qw zP((B+2fLQf&65r*8Pl7bT&$0*3J(Mg9(KJ;0@$xX8{G}85!BU1_bz4?0rAM z_D=P#_m^_@`)y9SRsV-wL47&8=P}+Fo;6H0wj42UK1szted0u?v(I6D?}OBLyKUv2 zzw}t|(y+pVjUX01)Cv$>8DFoh-#6%GT9+pv$$D5h-DuuChclr}^kpKH9hcdVmdQw9 zy!Gn=R*>1;B6e&Udn$e#d6s~YHDG%Z6K~@>(jbWiD^MidE^Q?0JY7F&+l z!ges$>02KfwfUhu9L50Y+f6z?E~%fo6N^K%63*f#xGRf3K>NP$zI1^~kq*n5T%cI< zS`a{_&-12A7s&*g=6EeTcvOueDRQj{BS*tGB;@j%1$#mJIc)C@K**th03ycvfHZPp zX{WX*^b0&RAGfqYhBw`#`K%{Bc74xPERZnEeh2J`EXl8Dmd$P^}Io0I6z5O{5nfm`3)pN_YEdw)~b!%l|3 z1gS^8vH}RLWy(RY%-j^dcu%5sZy<5|Pzd79TcVR$^KfELUziv!MS54r^{iq#W{v8A zwOl!K!6etmz(oyWzhW-=cmBTYv<2(wIR2l(0Hel$DgOQIv_Xu}!)~4;Oa>>+wPLzS zu3419N$1~90305z!3<+eL=XC5rImi6#`52elwB#7Guu+{@#L>W_;S9k!s^uy4{ZBK+q?i zL4Rwh#)*P$^Pw(;qYuaeZQh+1*aDu)jm~Kb+1nQ#1zr(k!cI5c$g3U#0$7LLfw5I0hvgP+R&>V|4`d4JM16IvCS2sfX6G)x+g}VP z;N3Hv7O6cv$ujp`+k)kML^w5YF>l0}1AL=rhy`}!fqWF!#~ip6CP`1E?b{6>96pkf z=#@)h-2a-M^<-#r#ln5V5Hy?-jI-|e(G;5|C-!NN2a?XJax#xAD^-7zS?=b?ahv#= zkT{PetrVfj#lf-!SI*EYLoXMtNEEmtO&qwiaLN*)8#XZZ=+Ff0sOn6W^E-!~H}#r5 zT)d@{Gi1%FHt%AiCDH2zp4RSZ;nP@U$Rnh~S0^$z){2$sEIF`YHuUB*@atH52ATx1 z)qzaI$1K59gP6nl;hzmE^rn$McZo>Ic2sDHf%Ji%ETep1=IauDDJhmlVF#Dt zDN`bNdZ?YqM?L3xmFRh9>zKxeu^j2~XFpxVncFlrS9uCyD#2H;9DmB_1R>=o+;(UV zxNAuogeWG<2Uua)xjGP+n60{aD`m3B zef7?(r4!+cvlySvjFYy?3YI;-W!9(he>_AZ4*%LOoBw&O#b-U<2E1Fo_>KNr_~our zn&v>AKC)P#B>VD z@!U$}w6KtXSlTEqJI%Oycu=N9Cdgol{rMx^SXez#wDT`L1xKt;|C*)I@_28+(UF>t zE*V4IgQ>`hDKF>Md7i!~<*&VcNErCXdl~4P<~bRblk4L@wgF%Kc%Ql84uJV=T8IV@ z%b#4|MO#Gp~9k>9QqKm_Ufji45R-OsTEs9JH@Ci2w8q z*RFLP>Y0HYpXe+b@ApODn|KOB9YAY*&C=>o?hBJtF+$|QswkeIlu|l0~QPnGJ?tUC!jzS4>PWR zyZH3<$BV>^sBVYo5uoVfVrGa|CTAo^sBaJs*ei;m^zWs=IxL+%KVMkfEd;J;52p>nSq!JJ)44*YVV4*lSBE6!RtPkYD{e|g& zv&{8u(75~o)zVehhxX~>X6aL`!UIefejO-t{S`|gyOwWSJv z{eQgj!bLl{riki!;N4P(L!HIV_Ojk>wHK)sd&%ULq6s;K5u5u@wDs|q^S(=GQCz>J zr$-b;(}Kw)mOP;Tn9AekxZ_(kc_7e3$J2hGb3#bAf&y|`v>OD*!H6&zynftz<8_%Q z(%^CP_FowQE*-U>c=ZqS#-LMtETPYxbZQ6EEFW4sAJRHc(M7IKyKfGcS|659ymzyn zYO|gcU-p!jmPlcL)eOaf`{&n330SEiN4WI)oeYl_hx}5_kXVi;^E6jfzdjZNUIEvK zMn*i7?-c!nQ4qP6cLW3%seN+FMF|}e6juL-u-+t$1>wB<^u1@^N{n1Yy@X(?gVfBL zt-zYTm(;f@hmj|tV8VD7`_-Q*arrz~oytHcJ>L7jyYEgyhqnR!lN_gIuW_U$fIy_M*Cy#eZ`6^&^)dx9 zs@bg!CB8X_c~y?Hm%ss;-f4qN8W!d{Ema_24)zb!peT?HSr`PfLE}vb=Y6#9#ax(s zw>p3T=PxmHZ)$R1iuLZcXhCLn#n_oAje5SxP!v$(Z0tBZPncL3Wb}zI7$p2S4xE0! zL3I=iy}jMr*OY)@giE9Ts1GNUu@IxAG>f_Ds;gE@#FJuBNzvH!;yrraJ$mpykM@F< zjJ?&vay_=xk#4J*%=@x(n^VI*Zz*?)9PPYog#|eEf)&t}b zqDlz=x_!O8Oh8Ku7eJ+?cmpKW1WRi%Csj<-x9v^2w-sA>d1Q@)zbK!z*f)$8yNdI= zymfKfk5qQQ%Q7pKaEy9q$dN8a{|Xc+=19A4HCCK}4d-k1RZ@DJ*J4cT$S`@p80Hv- zh3H-E`{SV1;HP8|E4-E|u;-f3BE~_F9I?r7oX>KvI!QePq9WVJ?H2WiYbLRwmk6_q zewMZ~^2aFuFy5m*_pBg^i{M#?cKI%*UWdQkm6%4|rbWr=aC@bvX8k`3unE<>($dVaJE)F?gYX92cop<$^p0 z1dEIBvQg(xsGC7J4M+MmZ|3Gki*AXE8CUwC_b#8)%oXU5&GYuj)6l=i3rik?C`s?V zik>JfFa=J0Bn;pDE&HdtotV!1eSV!CnH-qRM3d2;48uS4R7GFb4p$)5 zU-8}>1R6HtT;KtJ0ZSJwGxE>YC;)pH9>!dP4Qm#|b&QD^-DL(FuQCFn*QW`fpaH-D z$Iy6Z(w+cbyv~8?xlqA}yh540pJ1_*A38}`Z1G6@YCV{6Y;8M7THtr4fKKJz59RDg zdg_E9!Ii-am?OKE%irtv2%ss1dW=CiVn=Tta9&}{aB;i8B$+ngci1miJIcU?aJiZ> zGP=!a9;d7KS;ViYK5YOVFH2QwE43RV6S2Xm-pkai>3S{R&n38&V;`sKO+oDO?QCC9 z5`YG#KW=!Am?Us5Akh0o)0f@VZ2zTGm}+oL zJNXdfLRSY}cTrHW$jrx$ME{(KZPx(?)-hW3r?|HP>_12D&?0bh1>(>2eyifz?9Rc|(X zQpvb?(?$x+y|sh}5(t8Em@M{8H}~(*Lvd5%{JxnlqWSZBRZCJwB2mne%&a&c2t!3B z0IyY^fsV)v&ds5S;QLzy_Th+l29U}lxwYvW3V(HzM)BlE5?<&i&rr(YC9JwwZ`gB? zK}ASp@+!3Jpd*~9j-Fb3klPP3fq)fo>V*G<3br2og@+8kQh@AKK`~6upRYd*VzL?6 zz{C}dydWIhdSts{O_`UTZeQ7`!IU(?h-pE@NuSFwyI2cCV!<;6)x~B!&4Z0$#}t9H zv{`(5Wi!33&zFA&!uI7Cer#y13z2Szh3)SRFz(oJcKzoSh~@plmDjiH)fj!}=ej** zCcYydc5V?JHUi9wD=;exC9z8P`+Gs1_t!c8NW9p={?d*pI(C{DD{|^aitCuvtuPsW zlVtg4VxOP?lJW7?JVgv6;pK)VCIsterkt^U?9Q)@K(JEjp5sHyWY`eU+#%v!I%Z2t zYYco?QW6Cywzeh|$TcSwW2rku3P_MaMXOr&#Q}7M<-948oA!xfIe!TyAiz{~(1E~W z`LMI<>SdV%U1l}HJexDW{{$nac_`V`wi4D2^Q}}u9@OaAwVYdTQ+HP~zw)IduVcf% zx8q^YOn^bF+n-xI|XxKxVP8lLmAZf zN;7cVlqIZW3}E*b!__bHE0iG+YT6J{az@0P1gSlQ=SYTnV>_&6?F8m$@@t z?Mii>CmXD5C&9HrqC2C^cBA|>$P5%YDu7jC)?b!~>*cEeE^#sYK6xGDcpT73C5w1@^2Bl8B3c)nUA#jz$CSdI5@gE6*>Oycl2Ht_+)zMVsszbJ#=iAt9`zGFd)B&sjQ2QR__4R&A>=Kb{Ind06FG4u zDJK@L3!dSMs_u`;dT#AzZTGnOP_SE4u=R4DaWX;>t~3#=Ud2LU$^ecgGhA1L>z;&K z1l_m1@l}pZ?$D-87046yhmId`V2b(@Gh@JWbDEkioFp%}t2Myl?+b9%bk3J>rU+=w z1zw9l2fw=HuVkiL1j4E@qo_fv?i_j;EgZ)530Bqi^5AQtG(Tkd0QrAYtsL8;G2FGI zY?#*#7)Kx=z!6_>_(Adb-}CWHwL{fEN@=v*_qK5H)z}@Qj zYLbN0kuifhk^-nwBY1X#b2yzzDUtY#rp&F3Xa@OPcQO}zPHu#qPFKeOl#5DALGXTe zBSkK)@J#In0<25E8QKKs7D3>prM;tt*WJ)ep--%Vo9Q~`y=Le64OYiQ08-HsEa1?9!wn$X6-S?_ERz4 z1H>2Xysq-n~n2grW643@rpx_T#7b!bY>k>3;um zc{JJnQ@N5h?Y6a7TmMr6ojWYBjpJyso`FLd$eRc-^Xgcg5iGdaE(nE28Rk40z*T!~SOYU>>l*T0^ST+Dr0OJnk~n(z4Dx?ck52c@f027$CMr_?%r6Sl-F zZWK{e5GJIly{NxxsC2_L((UsomSR5TwOEd;kgUiLDE0__-et$|n6m6LjGqf`iU}f* z{IMWKRVl}d>Lxou1VaAqdT|QI7UXHn=5(%zBvFx`nIkL2?MRd`1d1*seWi1jodcK( zCeeRqJhi`Udz~x^u|coeo(=CCMb7;$rWTvSwv1811zab08(0y_YB!6lotH~eF(joA zo&_q}^!Us=@#+(f^wiumX8firxEU-n{key@KaArwdkmfQtPTU|!cB?5h~Gnly_fW{ zWSaVx>_y>gRoSBEe)D4%DA+Gk$w|V_9{XXO^}tnbk5~YNbP`3=&-8rD5)x8Zf5RJC zfU}}LB{@4|otS8y3rcU zG8hYUs;BiiLYlGBoKS#ZoK1C4=-scCO+>t^7E_i&4BllBidWZ(5;OZ9VOgIxG{BTB z@92Ir@d^`2TdUT%1d8a~fsVMN#+B3R7%!lurQOHe+}PCgvbsO>4yZu-_s^>z0}ais zJ<0%9(lY`yftVdGdjSBL4PoV0(9{N<<-%;ULv{%RzkmQI2M1FR-tHnsP%zk4YXoZB zg)eoXla{7=|05#L?fY3!oY;fF;qoGv7n``Itbb@%rPdK4NeIDWP1$laKYPpWQAQ~m zN|GqZR!)hRS#W|B1VWU+idbr9-Zq1NY+!79M;^(mShtQ*xVzt^Dir+B>LK23k`8W$Zc+pmS#qY zOpRFLG{oT&MfzY`Lc%^QTJm$F&GS&BP=#kCZBi5jGaFeg^!sB;!N<2TZ!%eok+Zqq z%G1}M)V8PnO;-|K01@m*6KOG1&d(*}1%G>a&k^Gr%UY(yJeSsX3NWr^SPBZI179J83p%#kbqHp z(Nn4`rSYV;TjnO6;Mps>(iajR0l@-mKT}9u{!o%66E+s{Oy5acj**p3xv*3V`F(6GyFKybs2^4{_Z}xST@L0>8Fu(zG#ZNZU*H{Wt8YWtYW6k)R|Sh zH>iB8;Ik)HslbXS*H!R?$tqdg){9UJR+c6qfoMXYDm!}W-!fZ_&W(d$N59&%Zue)&01+$!|`mjQGfI>@XVj!p0QMxc^d5WY&;er$dsj^u%U+S z8*;c`O&#PZnY=uYot^Ph%+@$7KxK%7XFJ+E%gWsJ_1Av+8E9Jc!9~f8=(z&hte#S1 zHx{9A7Z2GQ7MMoyoA`h4b_Aw*oovjARs@WN|c{lUoov(mM(--UtRYagmvJUhydp6KbV0?;kTz&l17+eqGogVkm5_* zdwJDJe-0G&bI>-5tt2U%NBLclT5pCp0%hY%dTajr%}hnu+qwl=qxy%R?kgwn0nqk?K$1nibNqhi|~ZAzt!f;s-olyM|73$EoE_2GoO{vf!Jf@_1lEkjy&OE za<(^pJ>sv?J!s8OmNH%4n}+yP5hLKrraH~kr&wq*mw+Ue zS`-(Ogg9JoOT_=_&G^(0+?8!?_?-WGw{!7of$hpoQUL7@x6{G>?5Xxqfq`m2-_n3T z)uiYAyj8!|^SQ>AJq9@f=zUqs@ewg^59v;8Wt;o*TOUgDVEVAs^)b(S_pidyki}J} zoSpC!vl*A=zwiU5y`RBk^bzQYg!7*)dLw%zO}1^5*hPa;v%@IyCsO%1iEHl?l6A0UP*Gbhd1E3Hgr;vLfpg=AJiMu5Y>?t+0rk^}t#6hsH9zh2_d-4(jhx?}QIfCDPvhB3OnP zy!wTMPq&eFe9BbNyD6PTR(aB*iql2yYWw4BQ1G@;zhVCY33360S%hdjUCD<@^_$PO z7`i7$u6G(!?5yfi)QE_o8comyafjn-iIb%TB5OT^eD<^POZ~D^_nNhp_SRu_=*84* zZZbny2mVFSn2hIFJ(LW>he(Zb4OOeCU;^lzYFoz)V|~}R>?yOzP$A`dq=|a1>0a8J z&mE=v6j&(Yq8iMv6yFVIw8tGS#dYyFGW;F<`05QM%5-B#Sx1YvQ)J$A2CH{tk(;^} zhx0dccT2kg_J2hjY`)=>Uh>Aj*IT2fm`4;YP|AHY>_RGR8udO@y2JEF5_NghY* z)zhM(GCo&91R*Bdj2$zzCCw&U7n6Qm^yjz|fzq>bSP4SCb9dICLkUemzp(H6C~dTe z^E9{eIl#5E`bz6)S-tM(h$gFgnLmDm%{w~W$knbdmrKlt^kK?Vbivu&8JRcHe2(RU zqXcEmH7ZF?6`yT~@E(&S%Pb7eN5{tb<;X>C)3we8G>_z#MXWG5Q0rI|rwz3P6S?%2 zJ`+)5jV*rTp~e_dGW)w7a91$jm6J=b+Y*&dOD+$I9k}CGeD9w^tF=VBh0$@gI+Wb; z2?15aY_FJtLL>tRXG~OHA>5b2IYf;pwT&rtPe>D9d|{Ei%of z<2p$TUZ!T@l-GaX+XR==_IVmvTdTe>#SAS@kSQSYez_58c6T~a@4R2C^m%#+63fy4 zSTymzqWO$*-mTc@(?8sS&2Fj_ zD2{DQ{$M%^kx@W#+2=Bd^=t1|6AhT`eYDSLP?-HVo3m@jY?0baKAlU4!B^&a+UQ>P zUAIgst<(Rw>n*5$kZrY#Sm6L$$r3h~awS!$fU~y`Lc<~?+Dqpcn;6wqvEl)BY-Fg5 zbu`i_k_Y-z#dJd^s+2&(F1z3uZVXsjM#4>MoJ7{+UsQt4amkoIL$l;~yIDX#PO+vX0#_P!kCHQ_sI7tuHzJg-p6Q+uLZQM{11^7Dqfl^-}|ku^N@N& zySNa*(h^r+iYI8f!Jl#DN7Ot%QN`D2UL>rB`X;72A|*y->!#y*b++w{JlNbPu;BnNq~!dVJeeMN}~yO*R2J1Xv?j0#Fh05-K#`Mt=i2W3%BDnrP^iI%a40|UbNu*6(9*g9 z=n56JU6xeiLNA_gI=k5dckZrSCO`4DhieCsn+~UgY2n+|4WKm^DHMzh%QD5D zNc9(GEWqfNE~bT%fR>%OyhZ%#EpWZ8TnzM>^l6)X)D_K7lcPF@Tw-F!P~km7f<7KmLxTM#7|=+epH zPZm@qSVgKElr-NdffrYYlp}TkugKa5#eX4HQMXbPw zlfNMJ_q^ZVmxTUl{vgi9f4k4!k5DmS*JM=F8`*Lyw>p<9PUz2CQad&~P3rb&JBHog z2|-@9unMoWpu`xvniQKqItYuFPk0hV_lu;Evehv{>-)FO`vnxbd)K0^lr+HUrigmD z7g~NCW{;v7M;8{#i05{x!IE)NN82|B9$F=|;~PD@%XD~3g{*2&YMp-Y>F6Vz$2Z9N z3&uXDjX-y7xgCUJdEJB|9m!XJg73{hBG)_%FQcSxS`Fm*S@#W#g`@bHjC>E`0prcozZ+1el zQ=>#>u?+dE+*Uj-aveD=$217B!&uSVSm!WX5n(Q1-Jw z$PYz!RVOVErMoDEX%7|~Y3mfdUVSX=I-Dxqf=iN8hGr*8MumU`y>CkX-UIZDhbry> zqfz;*rzhnqleMI?x5bV8xkBB(vx?LGNb?_S;&FQ|15xeEuHRgZ?8(wH;7G28b+2k4 zT|+G8Be+{OSx!pHK?NnXU4(o~$^jTKp}rBnWxCYFAX z!ASY}vLoYDmVl?>%kFFROTzQ>#_wD>qs8^w+|f6 z2IwK2nRKV=LM@@$b+qzHqIgj6hUqG*lyJ3>iC?pOnxB_GB-&jf8f7+Sgn$AE5)_Lf zh*C)cma1qQOma1g86Cc!hmyrwrC(63l3euus&=z6F|;g&RpS3uRfQKD{x-a*uMLKn zQWpHVnWFPCI->f=oKI8%C||qlKkxy!o$)*GWNKNx2nK(H#dH1n<^QuN!an z4jW_wHoadc`U{(og4?C)YcmSaw4wSTko$Mp3FbW?@15V=OznS3VCtN+8}U93=#M6O ze{_5t3OST15a8hWSn#+`&}nx#Yxsmu9(cRKv&2Q)Oy!o%(KU;DP#X%`tQ zGxPgkion}#UZ2x(Sr3ksbTgttvk$_am+zieeLVMMSLA4Mq74Wl3hwR-a&nX?_zai@ za{v^qxI#@oa_1t^BunTQ|EFVV`O0RvN?E2E(SKFruY_TPPL^o@EmO6~ zx*sDqTN`KR+KQ(;jzJ(-7hOJCTWDc8%^Dx%^6wsJ`D2XgAn03z=Wx zRFVh`9T+L#n3O+x(5jeXxESlgHgHW%3#a{t@sN6CsVXC3m$uSO!IOdRtf)eYv?f!A z0aKFIZZpxcckNnQ+_KbKSl#2NLSasF-Gq*zI|g*?UP$Dodgvc^Eq`23LlSmI%~w;r@2))ybdq)|@5b zgWqoutTl+;zSn8a@O}JjS{#zg+pphgJ0VZjesgZ^DfEWPQUZn1sa{T!gZ?3s7qDQZIXH zrJ0>|OGU6A4KwEp1^;=aNn2a7k%6Afj05(2vp^^&=1rM^qD>-{V0lDKsht#^7ltts zj(6j*;%Vnz&BxVodY)tU5UGn&QO`CcBbcAs1>gK~)AlH>b;)bO&y4(c@0a~Z3Ij`9+uhL|s>telz>pXntuWhQ^ws!D zSQ`tIGy0hCD8EbAAKok0<224?JMaY&nmvVIQvyOHU)|5^YDwIPr_<0AP)T?-Mb znw@dEo`#)kCyM1xRndwpl)!>f=S_o?lwb*CCI=rf>=Vj?HCF^Yb@(-kRD)&AD=c$_ z`o7-6k0v?vF&~>$vvTndEE?8%$P^!H@3J+NqX$O3)3LPmRs_LLNM5${Q9lodwBH7= zqFhGHhc)}Bvm)=XFKK+KzgZ)OKl~$WilH#C(27T@j3j;dJ@HSHd87Q5;S%Nn?{UY$ z&52KE*Ik2ZyO+HDiucu2Dp{2*4=@hl6sA|(`NWj|tWzvPRwxErkkeeU zQ}nB^sDVMHjr@53{;3-SL0R7G! z*e_v30{hX`Tdm)jC+O` zS6D4XLr8MnOW%G?+T!EEGOfV+Y{7yY({C`CiC_1YjutqX(-GcG;O2NbLHQTFh{| z7SMa&cl(vDjNe1vtJrma`*~NTkL;_Xfl1R=GlJJA*qkk9h*M4(j}vqEP2J( zmpxF{-ie9}2_WRtj@UZYIx!!0a%p~L{L*jefhQ5qkuF<~y`P&Ynd>WWZL6WI`` z!B|oNv(;om5HEf;8Hd|g86m<8)19(xpy{2>J^Avq8TUiQR#h**aUh))g$iAFZh}2x ztOh9>WGy;#DM6gKJ{CYDo9)w=$!gF)s9;!SQ%XBaa~(>qpKYit=)F@Il>`pb#0>o* zrd{jqq{Yz;rB5{pOrpqT-I|}Cj#=_FSZpZuUVYUSb$}AhQqXz-7?I5VKt)I}RQbh~ zb6x?>m%RZB|4t0brCkT9A_>0y_B%rU1+L8`*udm`Mc_{LIXi7VcUMND@+%7mWMLCe zT~{NTqKY!N>T(_;PHrY2(cIAMzWZ%VaKW>NZUFy^u;`*7kkFqrgZ`#6yq;UD+n``X zZ6}&otK(_ZksQsJ&tPGi?vtif*WB3I`8%?#r$1h0#~xBrI?4cdymsU4vokigHdbR6 zyk-SJ=Cz3e@f>{TLr4~t?PWOziFje?6T-E3XL2m@(y!}fXj=Ik4F6rOB{!Q*^iD4H zTY%nn;R2zMNtVDKY#jjlpZ?6L>h3PdV$p(b?=8MHXyI8FR#ulbiCWt}9k zDQ@ab`K+9#q1S<8;?jTKbeN@Lq?r`T1XBs{y%zRQz5hkl$M;BF6uIHF&7yK7`W#&|CceVeg3}-6O;duL_iQ4Nc7vlQa zKmIq}UO~@1WpqZ1&BDT3sBaZ!xcscXEj_O&*L5suP8kZ@7fXu*QRowD9^8tJG*QP1 zXTbM%-0*a8pwmI-1l|F|)FLOK_dh^vi8fegp&K z9Dzg9rpc&r+R))T!(f6~dEg4uF5s-$eycU5IrzEfcN__!Yfg7)cqqDL2M+HI#lvq6Q8Wz2;ofN)@;#-j3Nu?0M#~ zkWCii(<08iu|!&1M3MFLkI_wTak0=Z0ZvuB2=2R$ZlfIxpRT3DEGLwC-0&t2a8K$O|KTKns9Rw;t$@Qlxmp|Nby#FA-(AdyZvsm|qe<3fmW zX>`|*w?3z$T^zIX_&D_V^s}?yfzC|+ZaB(COFfoU)lLk4fqzpJEPis5aBE7`sYtG5 zO=Es(cI}c&Q+e*2<$y!L1e0el^vujFq`qQsj$1QxU?8-cK$gSIK|FgJRn2jAbPwh#>+Esbo1$XaK=5>Yl1)!R3OEOLRp%<43qOW;uL zk?-mMb~+yy(PzqaGU`Z0Rb+%bPgmuHlf(Nq-+KrnQ}gLDZ@!b0zpCeP=#~4(nAvD! z&H??QQ%XaPWsAgb;w{e+Y~#sAh(H)gI)dkYa@Q`xIl7F^+2Q`px>&_hZ(_8z!p)ei z)zbKm8J`ZOEYv#2dcXU(5U_M-=Y1{VV)Nz*#wSTrwb*x*ig&1ECtJaTpi(b3(Vp+S zTIJBQUTg?ke=9L{F1Zeifw!MzDv;lAtSp_MKOer!`YFPuLYP)+E+5u}07~D8%iEG0 zIk9Z?((5=NDN~C z{rtC@(YXX8f{jCJ+80vW!6Jf~0bk}d7Z#F=`Yu6v z4BcWA5Wn^wA^Kx;P-&zFb1ZJ{?D4=pUFVN0nx$r8XVZr1I9&PaS$K8nv?1E{WZZiD z!jA|V214Cd0gjBY1i|+DLANc9tv>ECONsX+3A7ay-!B4(8Q}nS*=}F{7pqk{xbku~ zm!SS9A2)6+g$nQs{a;tCMI7qtR8$(mU|lNHEAv(h{${!E2T9|rgpnYC5A$S~p0^Ya zdE!*3xlwFR32!U>=Vo7=P{t@ra$+YcsJs3qI#t@X%}K}7vMX!&bwgNkEgU^xdujYU z=K!uXG5`4cbWC-4Slr>FS!SzL)vGKu6+ePTHgrOgF4UJ|n`!FhE-wVMn#qzTnVjSt z6C)xbNSWAx;a%c|2`~qk$EpQvvuRB}+^MWjrjmuZ>xn`720cH?Mz|}(Z>()H-w!b= z=J4qtD-o%2r|3lW{g0=&j%urYqJ=3?nihD0LW^6l7I(KI#i7OBJ-9o?9ZK=w6nA$* zad&r$ySwF{{=VjRp(DHbekKaNa})Rd`}aVS zAwQeaKf;-wUj7C%()q1*g`{Bbhn7uEE)d^xNSv-5iy_E?k(RzDf%yvA4Bi)2R5;@B zVFdI2e=W{BPuWO4Y>x$D!pzHjD$h41K{48M`h>Ey9`?1`#rw%4G8`YO@DR0M60I1r z(%a;x4TGSp+|l%R(F5@)N?9HIGEBz6YTLYgm}w0aIjot^)zSBgIQ2afaY^owV)Ip} z`gTTCm~A^0-aFJJ^q}A8*p+a4@|hXANYWB4Ec^}nzTQ0a%<6SC!$Zn1BJ&0;{VKfa zbf)PdTG53mwX*Q5251Z^<40ir_zi-I_A!Fhltnaa^7^aW4{V7)EZ?z>=`81}imA>H zV0%m}X&!dI*&A`QTXdDkh*|$R$t)kqYiQyo1TW3?psFeNd%s;1R zcC0j#WX%MojLQoM-tXkUAS06JP`7C-Kdh~#c-SIpKACy#C8}D(H1|{X0I|KC%k}1 zYoGsKBXbSaTqkvA${ZNONQDS+DRREHM3fuH+a3sYGsxXYHh-HodQU7BN8ZY;$&+|v zqlJ5DTjwRkmZHC=eC@py2hkjsz(}m#*0+wh?~Sg+#LrjyVX~KfEA*R%lsGtQ$CM zV5C#uEtZ`R#4gK%iolKaVa~CXy|W%NK0lWt3zMx{3i*TLYL!KofEMSRzwmpDvI>p1 zgB!U+IvBhT*M*8`H{Z|ab~!sCjfLy1pk~3TO50bY`H7Zv?J1B;30M$scJXBeHT?&g zmT|sxN|@r@PQF_Wg|~C3J`pr$qr$i9c5Qje;W2m~*Vp^n8L7T2O1cARQgue3g*U79 zK9hWC^7rkq;w|nQkdvXy2Dtncc+p4%hr{I*FzWl+vBgTQt$oB z$Fvx7b@U6C!omLQ0P~vZbj%bLW)Vm$3g8ucu~iLXlh%^|KA=EiAR8o+h!}(|Yg0{{ zyOu(TgyQk{DjS>@T4*feEXFw7PRiu6oK35Z=lw@#O@)C~-{;d5A<~4r#6x7g$+&xc zA<~LdO=%SPor>RIHft>8{vcQdL@~H~kC^(Fv=tAOE|G$>OxCFyIq$+3fA#jtKL$+< zMnQ~PXo`;l9pjZfv=p39jYn8gW`z-;f0B3&S%taTwT4u-X1rsS#rTCZGe(9mbdSyD zFpe0kPO53xI^>W_TRQkzPgNm{;r3+)S`{_w?}BlcGrl?&;SBFSCnh`APrPsTv1b3U z9fSTckFshQ@S#8$A`!P1900B1G^zLQ zj`6f{CfseUE99LoT={DyT}||EBz_tMjQmrRh5~C8512dnUYy0=9?!$-8g|#GYpIaa zf0wUEp3_~JVkUPeXCa)+ElZWW=}$#0fkkWf&44G3xxrjos)`LaTy_E5ri1wm{6%XG z6!+x}Z<)rSS?x0qdr|1VZOR#RU;MWaW5<3P2+Sade)`DqaccD*!YuYpdWog0OftIC zF`))hnpOrVAy%{BiWLo@ezK{7D(v1ijzj4qmMIZ#82q;9VhCfo|c8JRb8Wr zGvlpv$Y6i4Wf&oVOO|ZTVCOMW6DNkhD>C3D6_Zo7yB6!U4U4$7ftZJ%zq#4g+CY4T zBo6enq=fX>xsk~uRuLmR{tP;!(2o$SiK#%DJTg5c$>i{`#*vsjZzZx{z>!X2$Gx!3-?f9CyE1KR82W*JYaR$t)ecJQ@uFt+DF)%pTBB6PVWsxrj~) zq+)+heI=|HvH#|*`|mG8b02rlVB{a6HkjHCrXvT5AH{L%N>?_qeJ}SmKg^j#O68eW z->1%9CuwtO6}=lBs|(qG?Vuy!3DcBI8bsMmYQ-ghxI96fWCV?DM_oRF5ey+B+$gF2 z4+Wr4$jj?C1FkYYhl?l%XAaF@(S+<&Hg^~cUUWe0RMsy~v5Ure^=$*{ti$^Yw67oT zBtc(XJ&ivf?qAknz5*8ubGh^so4Cuwa3r7x?>;Aczk;A33!{|T|2`_JE?{y)z(>k6 zAJaFm%XPA9z9=Id@j9XFGsRsQ_afh#{J`5it3vo#znpe(FgIC|;LzV`I>}X@BnJ5q z#-3tYsyak`nuYon__z~SH1m?gaIjNKNMM8j$wSE62)pn2J-2Alz@>Ah8U)|mW24V; zKZmp~XflvUC9B{~jer)kF8|4DrgC%@cLd*f@B)5&*14vojh&$F&G_bJn;=By=0Ozi@!j$efD85=yVNYH2a91TBCm#xS^# zu>6(L{DjhVX)`cG+E{EL)6iyHT>tIYLUr+LP3Cw>&46i2Zm|C>0&+un1Z`(Q@L(E6 zU(MEk4#AH1`ra*UNf`2MuS(mGiQD99!6bajr3&hwz#S9o>V*em0D&Tpbf-n~n4T(f!Cd3^TEp9U>$r(z_@Z-4LSBX^v1U8ZKk$$a z-|@%=KRGcrd2p^&o&FPXNe9 zyPIe+oe98+v?{l-uGV~;I~_@mMJBOdvM;)yQ8pr+oJmKR$UQX~gMbz- zEm{wL^@X`Rg2_Q)o|LuBpop?Y#o29EuIH|vgvPKsF9*titeYiM4$!o)?1O{^`iT1!PUxWRG>XSo%KKC}GrNXsD{UsN5!a4L6@ZX%RCqa+v6v3upOHP>Sm@cbON z?-sbiiU~xV1a7Nbx{FbK8Ki;Jl{XIme)BnnSU_H@lQ~H3(rG}6$d4sh(%(q0rp+^F z=k?l;Tv$f{#&vo^@PUIE{@Byqq)EO(?)?ug!*Nu0Vhl2f%|sg!V2_zYYz> z$7{5-^lf%DDt1+x3h+cFTspozwGz&vaO_wUNEn@|q_VIW{n!@~<&9pKM{6;TTvb@u zERCHGCu0c|{sQsu!{cvk8MYVi5kiy{=g9o^-;`LRzcMNqlj!iis6NPU;bR&3nUWRJ zC$tvN%{qB@awJkLCE^mkS(~BJNC$&&+`?G@V&H!*hAq-TQ_NCJx2YA z5C)IeOtM?7y15+y4Vxe48Vh;_0^v#Q7wwCtWH5|?ex@=Ij%qv+gd$)?Vhe^k@%Z?H zoQ1^EE0q;vi0EJa0g}9PgavbAC?ktQ>wqGyy>oGGciWql|^Dteg~CxxN(NTj^#&EBG3 zS`F9@?&Y-Y)pV&9v>oTt468gw1X+8tnCG?kAaFQzrz$T`ncZP-Cko?~uOGoKW%dVx zv{E+UD5a4n;VEVgQY^OsBd#<^V(^6N3s@}BNQ+P9;{tDv@5gT_fCR>xXg3={) zb~d)BXL}Vh^izUmWRK#+o-89x9jtr*E#eebY_3Xj1-Or^F`fLIlcVobJKRc5ZBQ(5 zhQU-V;C}1dKpZsPa}*zcXP0gwcV--W%{SN1u39th4<6ZHId62TW0GW-wqh%txc-rN z4q;abO0_Eiz08tRB&^(=!U3C=UupIhF8TholV2cWKi?^`Ost>9im$!L`{+t;dsb*! z-BeRlq6g^-`UVq%!BZ4%NJM4&$A6Pk0-t}U?AH+Sic-|c`yI~P=v*l~Rr@?#OjfaA zwvd{Vdal0I^bNzP)V`^spTE`K<^$kmYO18DLAGJCSxUBB`YJO59-^Ed0-&DEXB8yt zrk{mkVnxu2t6hJcb)E;jkKl{Maw}0guD3YQjT~jANWW=)yCX~Xor^C;`G%Ynhnp?`G@08a2bNKjAT`19q^ve~Z^%)p)O1{j0ZG#p7 z^b!?4-;0b{!pGm}(Ptl4VC1kezKb>io}oVp>}lA4tOVy52;auVbnB&3BLMU_j3{$OkMaL#2Jyi}pWYkp?ua}xbrs)vlZjVr+>Xe0Hp>DUz-Ua`JSQ}aDMsx^e%9tBhGjfD zC#2f*$9lTt(t#zPWTUUv$*YzUA~dgJN6GrmYv47>R)7NzG9$7 zQ%fLxBgn7*GIBdxt6n|{zl;qUMfqJa@zAj7JbYI*~!vz)&vB# zOnHs9{cn*GLpc{vyWHKnWDXw+;y%C57w#3tP9dU$jjB+#XB~XepZ79gXF{5(`pn@= znKU+=gD&ew-$ZM9DcvzPrUHF<@4&3(m_V8~#Zf9X=`St*vu-Z*A@a$|5{oj-uIaG$lSc$$ma6Ce$)3gi4#9Qxyv=n05gwm#fRM+I2IQ0R$*U)YJO!uuvC>z=#%hvR=2dVjU`6*Aj*wZ*y&; z@wv*P%B|~iK010&3ZsM^x5=ALCJ(&Xd0X)X8N^=qRZL=eUg+582*ru&mEUYP{wCT| zoIi!KmySz9e0;@0GgdwrGc+xm#*alG!AZM>r|!e6AHb`Pm5dno->FS)`M#qraYOk@ z1lpK8Hok8VIu>+wi^T)H5t;$q=iE-G(bjoTK3IIc9Cm1EKc_7Cr!@YCVv9bgu_Lcy zHIUc~xpzIRD=*}~#FJ%ABT!TE!w%j3fEC$Y5T8WopGq;8;!?ze#c3QgQ(3?dM)AA2 zwKC5|3F~B}bvoACjE`_y`U*6gU|hIl24{E8x2Vz*Qsb0Iy^iaMfG;vbBlpuueS{|1 zpv$A5@KMfCn{Gc5)oz(LDMS?8)?yo^x}4+kqM%>PmnH`ul5NaPK~nh0R&>vaoX*`R>R+i4Di-dzI>P<^{sFXH5lT z^T@N*hu>qwW%2yYW=5sTQ&T)bhU8e1U@|iQuK{L52cD1bVzI?NhYkI5osSc##}N<; zL!p~ryA3tlcjUezqdxR6c|ciION-j#W>(`DQmezu9gr9}7!v&N z{jFp`0l$^6?)3L3It*v#1VzOPnFgcbf6hM;i2e+>eD%C&Jp#Wu`PgUN?&Wy-%t!b# zW;;rf;%JSuCQ`zR$jr?&yDV`_+6uYM|8;0GWYWjZZXq@;cG1|rN0nBg&nt3&AXg-P z(77xtc&D%iz5kbDw(js zU|bsP#4d^p)u=M?PgqGpZo=wgEX!zM2YP@LH$C?MtvFrLg7bqyC7nz|k(oO_e!T!D zW~m;V5x>RR!19*p!+#ulQ%M<|%4J4K29z&W376;oVDd`=RH`}_m{~P%Dn|K!1UI$k zj`;xpUk5d=Urrx%W_DXWN(pL`^|RQg5dqq3v1!THf$+q@`j2e)iUSDD%E2E2xvP$0 z2SCK4gY?{4xYDne2qP8cd~I!C4w<$s{cPm$R-#Nr1F?k5!SJWpp3HW^y5=5WU7jE? z)A&927wu5dpQ(<dgtDDL-yNXPCz1|s2*+a^-uUwl->uZ@qq+*x&w4W{mjF5RP; zTP(qmk#X#~qURbON;5$!eN^8FTatHE4GIkW-%wm()gfkd$zhCp6l2$;p6iiAMH`T! zWwz#_V#~;=^kx}(Ti)2!YhDGOqug_l31{`QEF2RWie8?Kx3<^!Xf7E(@9Cf4>7u3= zj#HLFYAi_@;wdFc@2B5m1DsHV4YsblK9mSo+wiqEmQj3uuPpY~Z#A!J5jVR_%;l^< z*AVQP>!$GsbJB^}p_}yT$#E6x&RKNm4z? zMhG;DLe>G|488b9Aha-YhiD&RJ~f_OD|shMbf)qCAE4ZP4u<@ms{FTW{{N9hTgd0J z+4uR}Mq6W?ET|PXX*IWSn<jo?s!^3c-j%%q!jeHcd%mDKRBZCQRt@=6lP%xy^6%D4mIB%%EL@FCJq>jJ;M zF%cR0CzRdqKJg$P0A}vrHh0o(Qz{~HwNgwhn(%%%P^X*DLaNwFh^XoEq`4IciPUjm z%Lh-2Jp}&w5sLPM!r`OlX7K-iGFqTu)^Qo5pxKwN?X+!{cGdMor{f=YNiFe933bnUy*Zn`)jUMeYnyvy$D`qkKohCZWQ51z*-ctbl0;H91IpyDb0 z*$WX5OZU0pioMR8{R_4VD_~Ec=H)lNW^CSfIGwUE2O^i0IT+ zcBI<%LQxTV#Z<=qO-8@hgs)HTFQ;xmobx8%vZ=T_u?Q{gOV89&b9ztC7E!qt_OUen zBQ*p&C490Q!HAOrL2ojL3E!V7jG^V6;>YB829B<9O2n@-zPc6_Ig7kgy20X=-}%ZB3e!{0^&IPZkqp++FumqEm`lh zoOd;_HAGr@d0+fWDqgNzP8Br?(;B$)eGW<6nd=+>s8yy1foR8NnS04+h%0a`eW=`& zcBmP#U+ddFt}tiA9%|`FY=;hZ8VyQjgR-N6Ap|07N;j1lMi~`*#@iwl{F zVI~J%mD1uPE<}61$ajdA2Lr$UhK5c<;dR9(j({i#XJwkS#l_i){x~&Fr=62m?`MFc zuE5zv?%=wN$R9qgUfB*U?AEOVQjElR96_P2uy;K6sJKM0`-H!{4*m-ND5Gd+j5b)_ z3{l!xkCZ`99>;%P-|QvoA^ zR8XBj>jL<#9}3KB`X)2%Ux1kbENt{`Hcp4d5fq0E&k|8{@Xk3O$?()^h^zB zScazT+!NG_)VzO8?FRzB+p99mdY7xO8QhBHnNP?+h&JV1){8VJ%+hq8?AXW*A=+Cd zgXp0_ke;8Icb}_Nh09bZCyk+1nhbA*?Ir@uC38`1+>*AtqsNeSpFGJJ%FJ`pw1!7W zHuIgo2>c*N38(iXCEa_-dx?gr2OKRJJ5d3kz%{-TRL1i)bryztn)mPS6Ed^c(FPe;=1HH&PHh!>BK4Z`EWpR`OUHeIxS8fbVHyH@ zIudc&8&s}5LrzbRMt1JQ<_<`HjF}CSIl^e=w*;&REyU12YU6R$h*C|(N0NB=#l>#C zrd6mmh;zq;_9j$?xZgY#wOgp0ZL2wBul-uhB&5+MSiV&@)PE-hfcvznYvOe2!{(J% zlc3mZwSX!X2s6=owW9HQ;4+^J-P=w3&qPaHXEd^QgcJ5-g(6woYxy{IpVpc(gprL?e$ zSyA7AqQ|EXgccmpqwiRdV5bf%UOgz#0NzeQ4<01u^u2BFZF8_va%Zhxsv`fXj-n+i z&!gHQuh8-w^;`b3h3e0h+vvI(lh)DqON1?9dvBC1I#k(3{XmMvqobojFPm_-*ii3H zLZLfA`Ru+^`D6BBr4J@D|Y~E%{t}ciQDHmk2hEg^&A`=kNXPn zzaw0Fk6{W=IVKt@vVWOgHsEZLmlpVd`1Isd?;7}n9hi5(m=OHn=!k!}0QozGA!gyu z&NQ$KK*IH8`2{e%9UEgJ2nLyb5?O6V_EVN#s>ec>hM3^>T5OPmXGyzwu>D23d9mJK zG+Ux0FmuHpuTE~MA~x*RnCLv`eo^Na(A5VCEI|uy7(W+Pl+OOav=nsCi(K)<$ihpw$}zf^om2H{*P^I8sV7R+YpIiRQ7PTz+VM~zdP4u zX^S74+efrN0frg2d$lB_^uX68XrXv@?h85-x*@pZP*q3IhRfjz>1Il6Y(Fx?cZlP* z&!zU@jYtNZcVqvdxBKo!hws|=F8^5*5R9g~tE?-~RHXq-6dLn{v0rB`&T(*7NGDFA zo+T<0fP_H+4fnC2cmvi+v{X4z{7 zvDw<&3a;!7!&Im7SN`z>b<4lSWUZ3XBUa+NG4Tg?#2KtNcXTJh4e zjnL!b=k@)V0FJBDBiTzXac^Oxs;bUM0*Ch)E;ZP`7xCH?J$2s*+s)56wOX3(W1t+r zeNIfw|CdMn{ANT##lWy-x>tz{im*gKd*~7Dh(%19KGCwSdbf=loBmdi``8^5hznWj zq@fQ7D{s!-tcDWN<>aES??WGu>hjH}c^r%>--n^0M;$G3&Ce$=YD(u0ti)JQes;&^?6ekp=V7Z`VPX~4Cyz)|2>YE@+Dlx z0QqP$F9-Y$x0w^uF4#m3pNPSaO6{5xKYHN5kwz&A=I-!KxOiAoEyT;NG%Kr zX6|k_%LgNFp!m`;T7~|3-}nvfWzW;m_lzywTW$xC$N+RG5}vdcU!@3ZX7a4M9{kmo z_@l^MD+qKsQx|Wf0iC`4+;S5i-rstdToBkq)-}X0g@;u1h1vL@;PCxLFg-~;%IG)j zSX~PtWN@SOt=KV8glI)Jh2I6eP)Pt7&LXg7OcP^=)=zieDOqvw^2xBO7RUqj)~VFg z`AKRxVzm}i4Hy9CEEeQ!`xzj1_v@m}P^cN$Fq3w{2I_}3bUM?>Az-qb)CLiUBd96|8MF&NrDdwW4QjV z)^dO!*X#Dm8k|>lfjf=d?1v8gbB(w^^M$+WAmC%E&Ftr%@Ct@aYse4{_nM>C#-{Hh z-1^`|`%`>=wMJmV@aTK&Jz+;))O z(*u0mBHiQ{jh|yPPyV(>iK@)`^d@mYG^K=#D}M4IA*nj{lb#QlCA!MApiY7z(n($Y1QLRn5=> zgdImKU>*=(_@X*JD?2+YFK<(0qk0m}=(P>cZd@E2HuhBhXZclXJkc&@tEJUe_w-Hf z7MJ7lndN4XA#?|b+IpK6{iU0;v;K+S*bxyjPVyz_Xo&4CE#7_63@a;xlRkHDr3@&Z-s}L_e9%NIW~98Mqg{8WB&@7G8=^EdoISjK%j)(^ zbF()JX#!WGWJ=={0Bjp>#W&Nzl4gBHj8#ir^mi1Xw{Ld6 z30I7}PaH}3k-!UK<^t&w2=rp!59X7EBH$;oon(+kHkq_n(P?TIqYH2-$X2>{_(!8ZCHpUKuV0$FgngnQxOL*vkzk zqrJPy=hmskMORPLnIZE1tkYUp&j2Wlo4JeKGJ1Ci4Y zUGc-u`!egSWj>?-h%~_b)O+UYd&?zwQ-#4BZ4J%eb&rVHZ@*9VeE|xkf%|*QICc8> zF@1@s=m^RsO~k}yzE=|Ma|;VMsO@K3o;O6SXvS!O!GQ1cr4ZcDoSha~Oxpchbx9tk{gP#k%RQUOE1OpENte;g7yl=IDBH&n}liRLjza@8rKz59i*B*L;izz#i12JC8YG9YF??x zHJD(AC&MuU50D+9xQ*>3~5GY(_i+-VwKGRZ~SRHTnCUf7AX+j`JAx{U7on{y8vB_Wu41$ z-BOt3U{+Jp{;e0Ndk6lM$IGh;zw~`ce|edinsNlv9OFkBK35y> z+m_Se?RQ_+LqtUTwN%X!dYRK1j}|$Shd!WpAs~##O48*4n>9c`nzUJx=tu>4XXcX| z{mnf8jVMa38dBKyOHw*ycZ2gSJ7zAf#1Sl5tyz58*Q#Ju5F<+u=AnD4E&VT9pDfRo z=K5lWTPRR7jB==j13NTa!JBK9oU|WCKKO^#R=j=Bi4-Uz2cYBiw#B_n1M+$h2ejEW zO98K}Puw;KwS6B26b?|S<-k8CrzbzLnxLC4iM(Z}vhq>#b)qIb$}GZvQuK$d9hl}M zjSC_P&$RY&6Vzulyac{wD7is;8wRRi_*@jmaz<506IC+Tow1l+=CE2wb+i#=$K#Ls3WO3AZWDS|UD5^71llSpJ=yr#HsYvqCNtcKXsWBCS(LPz z$J6->Hdh-1G+>HU{U$0!J>2!GESMsMq*>Luk{q9jrO>JnBmF6w!Y7X6#Z^OH1%^ zL1D$NOU16!zpAe8Ud?BlLVDl#KIzo^#8Ge9^s+?N`MD2%XS)jgAD@=nm6e|FrLH0YhCe7@1H>VD*DZ}6; zMRI@+9Xab^?K;)|@&_G$^Si;Rfl6Cw9W7?;?R(JZNxRX|o6|*FZllnBuk8|yV|Sm^ zMrDufV>N1{B$xdLM%GNp&k3@%c)Uk91XIQ6+DA44^)njaos7J^^Sv8GtD6KJ&i$mJ>Ow8MlxMhp)R zKlkW81;n$yw>LH2fdgJ1=UXeJSe~9O1cY(FfA_(jkKf4TIY1y&cX!iB82r$Yoi`|| zT8UqB$Edo!u72M>@7TUvRKOpYULGpo8#IFF5t(=0On}#*#-I`!nf9DsgN+<(3Sg`A zFTw-JBq=vgQDyY*2VIL*xKd=VZd@2*2E+oJOZ7E&N-9p^1-=isQ|vht{UtRu! znPZ8#<QnS3!9b#rk3S4EdTs;CZ$Ed@+$bufR{Q}e5z8SpG<=0g z?@3GZ9_{$;orE2@Etd4jO6oB0Swl*u3cJdHQ&;oX+R}k_NfJkF|VmPOSSe5fU#F zDg+xJ@1HkA`&BIuLt**ib24}v@4fPNi%E2cg}`9)?&;~H2qu`v^~};q+wJH-!!PL~ z04e0`v)6U|J2NnVPmYZBIdAiPZ?VoBft#J30NKv~^x1@;gph=U4ktW4ofz;J^S%1r zr&@6mB?dM!iraReN{OV)V;CAXb&QLfIzDF5#0J}}wsttTot%tTcpsl0LJ_e*9o9?l z!3%Q@2lG`$j@;7gx3F8g>~H0^&0uh>pp*HYamMxlQMgJ;bKC$jmcYZ_+yOSDGoMr! zrnk2M0678$mUjIYf3fMcb||Q~m)c+W9eM3+qRHs+Fkr&r>|7VJ>)XGikBy2IAnfdv z>vFV*f#|+B4wT{_Xm-5xViL^$C*;sD z=;<}^9?0M+JmQwHPG|Pvl(tNG>Dzw6{`}fo#}o!emn z_WK7%?92N`qgB_wM2ywCyq8L+@{5EJ7om3Fw7=rhFK8V~mdzX15OyzbtXDjstXKKP zjM`oSU)c%nS8qvP&O%PL+F@|ONOacnB$0ve;=U6|J`_v!@}O6g$PL!1q5fp!L)3g; zLgPSRPt=Ek&{u^oI(B*RHnxu~0k4GS%e2mi&2tdgBcr{ta_iCOxqmk3N3*Qme^Ei) zyTFx|Da7F1S#NLrkxS_be8?B0!RWY(UZlL1b=2{Hg zG`~67?mxevhOX0DhKDCM&I^2R3q02C%{nLo#+5c%-cl$MOyl+;CWC{hpDcCMiN6sL z{BAvnop$od7R?F;L`M+|N3o*fOyh|lKql2(40ts{pZXbCHabXG*6t^(27d?iAhFpD z8xD1;V|}MX|K(B7`8Xr|Pz-S#thf<)4C<0T7&>te?y9L9(YXO!EuuS|+)RN%>z^Jc z4%W0W93l9M4qzYqKzztsm+xGn>2tVi^YY~Cs#90>2$aTO#KdHGY4u(vN|=r^^+Q7o z)oip-)nh$A={4o^{Iwil$`^R`>c6eQB>hQayxv#}3D;A>v(;PJW`?H?T@oEJ@o|jc z!}Q;-E*%}>YjB{WDx+Fo9}QMjNlAcXd7WY8BW!JM4m(T?0R89Zk#~FV?&5dv+uC%b zKliG)^-FXEO}jstek9gJEXD793!oQzNrGPp?d&Ge4;6+8D$}Zf*VEp1j9mv)VDS+| z0Tu9p{Ldi(&&|xpC{ZrX*o7}vJU=!IDkd=xZWqsnJCcNnB|g(lLwkFB8*JB>CngAx ze>wh_l@~u@o?BAF07Cp2K1FV8pYTO`W;(v{X7M_a>gI$vB{|u*O(pmr-*D^wn@`Kt znJJ%KY|L3@%V&7n^VkCLX)Nk6*zf9;9Fh+|q5u{kW6Mi732W-lYrge|@J1FY4gvez zWz4^)tH1Ef*}f*V0;9#9h%cQ6!UeY5vjRA?bsBk4=)mino?13c1n{&Mpj5Jo#(yj* zuJ=RGeOp z7!xZt=g^A|(|?5DdomppW|*LHOgz96$DS_INgEweD;`<527x#@!M#zK77vNg>~lnc z%_=bM>_T7zc0q=pu<#<$w@WJMKj7=u(m1VrxJX@l`jV&Uy~}6Y4u5gIhr!EipDT6K zNcC>)fNd!>G-mONTq{l^i42EB+t6M28O!6B$F-H!RbWSHa7bUy#9oz=QjcL#h3$;@ zZJVRT=i^7%p$D+5P07qb-P}pzem(>jcWTCxgTPtvD*HCAQ7_On>Su!slPDOg4e+y!4Q;bs z>)+e6(s|?678G3hFG4nw2%u;YxJi~1C@tRJmOI}FMxk0!!UXvkCcxX-aLYv>xrHwA zlNxZ)*2La$U7K+^tq(M+Onv+vP_s${V{U4rsV*NO(z*BwY#b_}pr-gy;~{AwXP?@@ zg(tMA7rA6*BB+$bVNMpkM;ET)C1LBdraZ%<-fircv(tzW=p;p8B_8}v@e3ppgP~Dc zDJz@7V3`+FfP0f9qfEn}9JP#8RK9fNEvGXNA#Zwk0diM$1qDirjq??RX?K+1j5^OS z4Olh&Uo>rqCV1K9(f`F4_sqV_MeK!eFbL{dz9&E{p%`kz>l->Yy>Mx;B` zeRS4F>>DyyY1n@wO^Q>}1egbo=yELQFyes{kMtBTz@;O#ADII6JZNzSquDWp6U8YE-TR2gCRcey|jJu&aRp(k_twsXB>^0nVw+Kwv#`rY`-<=B@5S zx?ZERoSITr{Q5G+bOXs6{AksFbUrVS9t%O@toEh1Drm<3@gQGJ&O*+vJk{&DLN<6U zQ&vbY)!^u;g=BYe%gV|sw9m-dF;F`1q3u-QLJxj;YCj6Uv29P5-#xfQtx~V8s=`7b z>LVc`c|L_dR|o-pFgjZ4J+OMr6);t-)O7ZJ+BAE)Qh+C65@F%uviUys`QDrP-sl0v z>iw{pkJ9H6ywp@JW;wPLD;yEKQR%#k%%_31?#**zKF;S^+vjjaO;C0`0Y1--FMJJN zqg~G28;FUH{&vjqP(CWMN({U@pUkgzKm(?Jf8N`RB8KosbL4ST=p~}JckBPdi1gwV z9Y(S2$1+5HpXcC>jg7{5DByfW^m8{fguwNH00{trgwFv6)MoY8OF#!r4gMz0Ku-_I z;1r8+P;cc$R;#1tL%Es9XJ%%$+&lgV-!?l7(6oP*p6J48-)eptP(;L{A7YNAqrkFT ziVn&AA_>wimRDCVtS^^BDZd(g#Wnh#*$+)6Ye=KPSpExVK6mWB z_F8Knp6nh4_ygXVc=yQjIFTS(K!4^1w9a5JXQV*JS&+o8o_jhaD<;bmRd2`n(h+#K z`pZ?8>&Z+>*4bp8nAh}N+W=J%e7qHrVR zTddHxX<9?MU900wMPrr&*x06C;{C(WA2>lTtA93}R}v0WfnbZ zs;XMb%dg_(TM1&`09I^-85VR~8(YPhWSW|HDccU-TC(kew|smeUw0N3WKl2@^52Kv z{CY2k$NO5YfVQoX#n-)qfXGhG4D%D8|9s>2C@P1sPDVNJaV(hR+0aV;9R)Io7!!%= z%Wp_7e^i=71%8R8G92ooK}YVoNV?Talb;gPfI0dvg@Z-&de#Y5MIG)Y-oy@DU#jV* z=lyH<*xz2P)PN& z%Xn^*VmfI%j$hgC!$VwZAyH*rUH&cJ8|MK<;tVi%0Owl-79q3R&=x&l(Zfx$8`b>f zU(tImQbEa^yS=idCEp}rf@SyC1&KOABkQLdZ9n&`9{La;sW&Kv`hAm6qNEprTy9O3eFC~oXTC3luYv+vKkpK1=apHv!+Cxi?S$kjls>dV%1z_TTA`a(N zbuD`}KAsq&@VixmA1h}BV1>;>@k7{Nu^8ZtE~FR4_7Ki7)Z%@iwvjgy(o>pGXN&;C5a zp6kQrv*fnlsjuU0D1sYl-v-|IdY18JeE9b^LMHpk=ot2{5 zy_#OLyp7_5|4f0dI~WE72>l!l4PH`WXk=p1A9$y+shahJbE&ZjP~JTJYZ4+ko?FHlWn1^AIWiX-AGl4m3z>N!`$!Y z*iMq)h>%KD8O{`r*94Yo@n!c8#&ASasB=rQv+E3%n5H>&e@GgG`GuMg z(>uVB16^%j66reHo#rza8h5VzM9#s%L6##Eap-Va)^gln?(F@m#7Umv5%@IEqZq-K zgpWKSkdRLuBk2`9s}1-Q@OBI>DZq^Ov&)3hIO_W_d5~p>TCu>vYQ>GzVQwoojroK6 ztV~1SmX=uv-Xr^>w!DbWGmWbWv3dRZ-Y?%5b~eLuMl?DEdue+0sW_J1i%jIeIBzlr ztrPGTXT=JCr{e>lhN+(F-QIk4ytk)ovbWUi;(&aLxJMa_?T3ING$fwvz2mZ#!0e>FB2R$FMwar%L51Qr=)Xmtn1gylK9G%hvTNr=EmcKvNU& zvkv@D?H_b1WX%W>GV#NhBxpdui3)(!=3m-&volHhwf!FOMIv#uCo8xtEd7!5QB={e zaX*;f*Bx*~)T|QZAr69UM2kpd>@Cdj!WfNN(6**YcdHJkOQT{79{@Vg*OVDqNd|H{ zKDP87bu+L-WTd;|NtKWkbqEX7y9yEh-{S-~3!|Tm1O4>NIE{GL3wz=^E)u7LFmw5j z9AKL%M$^;1kv~xqjSjvlphUl$)YLZRmut`hb)zQl0IxeqXM&NfL5DUnzr?`sd$ra^ zi1C|17Ucc?&wlsG^lG*VtF=yr>xdrgl;2hZR)9#OdGL8=XDBd(|0k}$2ZeFY{?*H| zQ6As5pSQ-gQ5;op+g{zY4uk|i>0+J7oPHDnlF8fZ644wh1^itc3 zzx=)*W;_P^Uug?E=E#DOxk%n6jJK{7?x#gKesY>R?kH=)4(YOeJygH5*e+DC>s)Gm`z9@HT?Le%DJfaQvaNw>`rq{r>%W&( zr({=$tL#X3w>;WRUpUx1+4C0_Wchm`j5KSzlIQDj<2b#6vK<&`>{f{V{b5(X8VyAq zCA@tw^OC32iaIxnr_2N=L+aP+Y^CM3L4@>Ty9Q2)eHH(Qnc-NiE%ackZ;8Kg1oIH( z7KG!gHk1g!T~kB3?>1D;F<|(>(cqcIanh06W1$2YFDgbNu-R#*2C?-!6Ox}1OcE1( z#yLcDf22T0(b9;c?c9v&K_3*Wxc95^|K_@@-u+Y1IPEwbjLF=3xI)bM84O-QTipS6 zv;QtPx=(q>5Fi#xJy-}b=fAtzBYPV6&pg_PRhRQGj=nllqpPhmKFfKWTJl=4x3u~G z#-N$gRFRV9gQF4&KZBgjlwL3(=Jq!aZNAU4dEB~%u(5(Mvl0~UAnZ7SGG^nA^=e42DQ^C;PgzV+Zj(^hjn1e6ahQv~-HXDNdD!I{wO zc$HvJhsfujDCSIMQP$x8p&j~a2{-qnpO}7q2>&wuCa1f8^b2QlW$Owf-HTu>MNKAZ zOT|DXE(sJ&PFVKXP)6bq&l$hXQ zi7LL3UuYHQPRB5LRsJG)|95)%k+D3Rj{D@^ReD1^mSs9Cs1~~x=f`D&q39)Z{ z)_Am`BZS)kuTOTq2>lgghd{X0&AlIKEt;ex?aV-jZy0maz-JAhs-MfgxS?x3P?7WB z2^+w^++=A@{zOfPp|Xt;oYywf3@iefc`#umAoRG}trBE&T&oNhtsODZeU zO2Y?7#e{b!3+@fo>mM9#5!2^s?f8IE^7c%8dMRVSmodTz#SC2AaY|r3eDf0jg3k~b zxWnv{7U8!%fN%&bDGNwJRQOlG?-q3a^p3uO&K!0uWcxq9r{5CcUl#fL)Xx!3E~aCd z!Pg^OmOHT{{EQQBCrKz1YW%z5bL3dcWb^a_QT9wqOxi=fke=hskVBR2$pB=WI)CU5xf_O8Ie+zDmd^k0kZg)=pUKdBP730OBX*TW}i7mjjfsf>92~BiIdvR@L5yY?-i)@T$J#3v8m9XkqwUnU{PFat<#a{!4D&6MohoT+ z<(Q3Srb|p}rJIU%xvVUWIi|hba12@B^kgojb7g26pg@>M0~CoQ7G1a>$Fx$$5?>B) z|KHnTsTQpM&y!1f<+K)t6$3*R(gina(j&cO^D@02UC>k;pqOv*TJPPdNFmz)IveQk zOShK<<9l}u+thil_q8F`5vKFYYsrduu?myo>>~{Itg# zG7XImndhIfXk9md3M1RhEZco%_0f11-nkF$C<Bnm(5`QWHDAc4){&k(WKjcRcz) zUlq`+?J!Fc3c`HLyf|-}-#>=MEAn&$4+0y!&85@m_5IWS=4*heHn$T(gz$Q^?|vr~*uE8wq!q8+DTR&ASMV8nFECl#A# zY8v%=7{Qv|fs!tXNG;70hDAj{^MEu65}5c|z8Xvl6?p~oRNLwWFY5SrcC#wUcH&05 z&~_IOD9!?BzQ0B&G%yt}jk2@kH-XA94cwCDi68>%ARaODV9=X$GMCbI)lC& z(eq;|UnSB6UP-N@$o}t42E!?U9nd+C1bv9goO|d^^|KLs)OZrUN}l|V9tOP9p+>_d zYODfuUI>@Yl_{^pLs_k(Sj@^yGQ`tK$^DLfZKS0}AC=TJ!bweT04JXHz|COFe9&Xd!dSYmTHh{`Z1rA)*nsX5 z^c?y2zcxP%j0Cf@YtdjOCvYZByt8o!BEh|9fGJ~=j$4?-cLGQlEF9Rd|83W}Zm890 z`8ye0$^0Azi>o$UiRDu!^GlWVy;x<)Y<%u~X^x6-FL-gZL9=lFdeU0wq5l3jc- zN^`v3Gn`c9*!6%$x{@@yc9Ah*YNb9zC%!_l3n1dEylXuU_(uAwnNkx z^avm6A+pNOFFcm-R*Bpyj16TCP#1odC@g#wC|mD|5g>kpPcqJ3ukxHkR<@hiCK}c) zw1!E7VQdSes8r;cz*O^gj9IjFnw7Zh^Xs|Q+S^4FaoV)$zv-Hj7XLG;Whg!2=Ln|# zRa8=5oh1ZV-!CixA-+H&9$Fp?{}xhA$7`a+Hh=DiawmJBTz(^c+-CZR;wwUke%yu; zD~-yUOL;{@{~1JDPyna${+z1kFVJI^r`(eKOm>DgL?aQ1_O~U+^rwGg=BOv&B)~9r z+@`3dYu4oxtX(V6z+=-3e{Z|i=i&y^_nS^1kFbOts7;R<$}(a-?wol(d~OlsuZcnwqyMSV&vi|CVE z#eoL;kdbuy|4Ch?gMwT!jrhdA+8QTRuUX5;^Ny0HQZo4+u67MCOt%(w_UdjBW%Y~l z7~zHlhXZa?OQos@&#=yDP@Pr?NDO{lxhbhG`|!1SBIahz28cpv&M0tPMEyrYzc80F$-tm5NHoKT8?KsMOuWDTK4PjuX zUNw&{g-}kf50w**{QAB1N_K6CJ`#ah!i&Cu>yPjZ{ZhqzRv8D=c^uc0Fe6uSV~w<5 zY^z@g7HgACJT6|@mo;We+S6&Tlpj~a{ub#W#3oxV0goT{fmm2)BV#ENPEw*lkt~dP zb_(Cwj?A-GEe89rH|i$IM>+B$hv4?34YI}=;|AWZvCqIv719v_dN!il#^A44( zsmoNe6g z3ar7k3x8$yo&tThUfJmCZ#@-ex(%1Y`p-?`d%sJ+&q0OkD7;nI`Hcp~pk@t0oz7nL z&^2BpAY(O`nJZ%jDqB7nP^&Q;?v}dwc_y`+2iN#_+%I}na0wE&9{!@PZZ5$9fPDd$ zwRPs#nq7YEtIj=4-tB=pNO2Dig8VTk5&lkvQZMM@2toNWOU?uVA#pKVJ7_aNq&vcL zSBqLz&J+oDTqg(SAiX5tEn}Sw8xa|S8heV4eDD?7g5#~a#%YH=d2kxmDgSg>IuNXV@9(i%1&h396n4Q0WE6wEp1S&oHB$C$#|iLybAA!%foe@;-Y zy)>v;eSl&7W8BF>x#PC;^4o#Uj|lSCj}K7QE~flwS8z@)c@`6zMFuQ}OFU8|PCvVA z5nmdd)0VaOS%dz!)ZtUfGZvI~uRuDUh`knCTqa%x+Q2P?XK+wZ(Gu?u>}%?*36~+V z(X~%ok9-O3o8qr0-{6l_sR39zc-y#JUn)JWK zjYw&S9eREEY;C^m%X~r7}PN{q@@r0;oJ!)w^Y3RTI@2afhSf`ie_OcQq8+A;-PPRwLM|UEzg;Z z5fuMm>Fg1W0TImzw4Aj;Nm_?Gnp z-ILhkhs7)i1b3k_z-9fcRn2=|>+E}0A)$s9!`R4Qo+Sc!U(754jxH$DU%z-iu?ud2 zf&dHPv^0bRf0Kv(R*sry7-VB3KS$ln!GSud&n!9&k^uu>9e1co{JEL$r#rNP!s}zO zhrDfVdC6a@s;JQ6GTo;RR&B#TLuy@z-OI7&zqgTRh8Y?qCaP zyo_6KNwDomIcMLiKJ34nDRuQ_129=yD!4ib399mW<5~V|@pp}?hG!qmzjyD&`cx*! z(b6c>Ib%=A(VAVmvt@Faqc|$HQ1GpccK_8!?WYiqe_&1rc`2Jm;C8bL>SDDWVz_i~ zP6R!d?)i`wYw9z_zRIa@8aixk?eJ^;VY^FSw>jjNH+pRBx!7nosZ>6_eMITA zd?)U+%|qBdvnB3%wmZr2xbpmJ$^VLjI*v16OPc^1R2(k}kQY~7w$~`1R&KVkFV+)= zt+<7vakiSvo!{?Gy#n})i5hZmRmK}HruK$Jjs|>>9u7QPVRcPKZPjsodwk+gH|cN0 zF20K#Uhe{Q>2HW#KWs33q_PBoXneYkb%sj^lR4GH9&Xvhv!|0h3XZ-X&I2@TflQy> zkIVO)*nw9~%Vgn08;xHq>sGoNzCMb@`^(`Az4X`+8A9#eb|J7-%#otLG3>c0X5>ru zWYwwoW|=Fnc(SX|^((%QF78|p%9r+B+Kd2$9rC(XF`||hk@8xIs5ggP=ZRF6J`BVx z<(QQ9H2?MT$Jh{6YAL=RziBho$Y}bS(Kl~GR!Oh}Grp)zCK57Lkg*GIx%uX+NHeHa zia=82qBR()UDmTPmoFogRWfp2spfZS{c1?8iq&ay9uwTH42L_T|GOK#l!j0eIWhdW z!!rsLgbad)Kn4eqky2&s3HeF^Mvq?(Rn05#>b?P|Zh9C9yz=#px5WX8uIcGOORVLZ zV&nD6K)%?bKdTjV(3mD}*)pga41J%vobY1G4U)6!#3p6a(l zUK-?Yyz)@f^x5BR80^K~9GqPz0N#mBE4)Gc%JC3zip zJiVfznDi)BXVlTv%?Nzdk5jk~CllWdAOx9Jyx(*SyE_Uz-jaB#Y2s5I$L!2I)VD^4 zy0oRWcsjFF`aP)yLG043iG`uRwL8Otp8MtbQ!vKL3t+yoXudl0%*Hx=K5@lv;Q9)3 z=eNAGxYPoLZhWL+%Z}tO*S+%M?Hpms4+}}^UAOsOw14{$-WiZR9LmK`Sn4k9oOUTS zllFatDz2HV22DjBYIRYkrc)cE_sF_aT-wnA9z zYjMU=4AYzvO_5TW8C56k&?pveyCnVbvFlZ+pWzcVSgmWNbf% zx}_J22HMKdNb9wv47}g@_>swYnweRNjC2=)=u;Cp_zh4qhXWteotrC`)?8989mBlDpT3ZdkkAI`e0}t$%vooSo5>dh+hs6%_*3m5JZ}ETGXpJ|_ zb(9PObpDKoXtg03&hDHUlo;AXKs zRhEkOfNlXxz{n!)QtaxU6uRzYw<+uM)U*}Cm76FO^9-b=jcPF=1#x{K-<@LtL#uXmAkq-&`C1Z-Pr0}ug_-oeuOjM9e-XpTpG$wUy*+=v za3%x+nd$Ry8EF$p|0x;d?xupnA>BW0g^6i&@#4ryl%*0Y-}BbH+gjM#e$1sBl4950 z-zVSRB~A4v1)5NB80oEDk%2%;JMHc3Q>86{IDVuC&6``1_6QvkJn-(}08hI#E?Jx4 z85x7`R7?s#8np*fU@F&SdI6yf3J$8iw~Y&|*;+r5Coc0A8hF2}mF$~u^03x4y26=_j)bb6U9(N?)K8fuv` z!)K;=5QD}<7R^4cnu}v$69wA_&Cppeih&6vfLx$yOElwa@5qR5G@su}=&-t^v- zBPXV*zCJ4{Tf=_kS$X4b<=EIZTbIUHC$NON293v9Wk5= ziqc(Q&1+ylN`rDVGg`#>y|$J)p_dwddedCyOG>tfjPwNPt5#Mbb0Kw?QsA6DM2`i) z7fjX0Oe^pa^$|Dy>DXvPhs=Yirl`o6KB&2i5*qAMZ&_gXjj<0q(CcU@Q!K533EkGI z?CBSglhED1=CJVII-t;Ub9eW1o-9yjL?FgS)XC6MU&}L~p}huo@YuhGLsC`Ba&v)` zJDzK5oac)QJyz$}7_T76rW?@qYG!Oq5p&@l#d~opU z=%{P?^j8zja0qbX4|!WyIMU+o92`)RrE_A+XeT`^1i~7OSP4suiwULAFRDG z_bUbi+B|z=9=$}@PJUnzPOzFRnCVv++Q_~ITa+|5y7$JTjl1(Z?enV^BbaqG@$f>* z4eQ1~iwh+tNTFll_BPQ|;7Vb3{JHch;hOnb>F#CdSHiRN!nQ-h`QX#oxgNVvhRw?! z^jf#emCfF?F0LVO@plhl0GKW{_1A{&^njS}jIfit+q5!8&uOkCXG(-3h{NMGqZnzkN-LKF5Uu6q3o+{=}!>2z%KbdSSygVSd?ulz#gE35(2t((Bu-l$bU zKvbs{d}3s59#Ub(YgFeTWp0Vt?ydKV>-FgS`jV0bPWS%G#-_Wrr-$sF#(*vYqPl9{ z4A*XiJmrH7)rx@XULVWyRSveXh6b)QxIkm)^3w5Y@oHscA^2A||nE?#ZtJ=NJ;$wun2#t&}tsp3eVU%p)MTt0duv~jJ+I<{c6JTJn@g7wFB zn?TpdZzJgl1MQ))Y~!r!aF6`tHw+~Chew%ZX#cKSR#^AVq0)FSE z$2`ghb8~Zzao*{_C)iKNkJtgn{=t$U3;b9HKx5H(cW4T*aaB_|)%)!1ivdx0#NZ&- zC+W)B`(!qe#|_iRDm`9fGUTnlGL(($_h-Y1AB^K3Y?(vAmlQj0e`MZ;u|-696Rlc+jCfK*lPe7pb~2BX~DPXWg2wH##O@+PlhgK*N;j!#crs8;q4 zDQ24+dR)KGzjU}cE!f?CuQnX%i>mkYn*n%qQLNI@kOSo6#_iF^q|CxhZDpnT@UWIv z0NQ!~MIi>W6iG7jMaH(`yj-ERt-F^zw}5tZdg{5J_!O~&;F7>n5;H9uC@9GzyjE44 z4L#PKHp-TavNad2u8GFN=p6gAYrvC8QWDIK*X8nA?Tl%{ymB(%(i25&L>rmPMOSl!D~|jeyril# zzh#hZuWK@96mVe0pe_3V3bJPxe#~OvKd`(9=>mz{3Zkrgm$^u$qnX_Zf(-2=v)`l& z-S?2opKvAbfkLFShwb&JVgvo2Yzs5%UEYoNCG}H!t#B#LdT(XkgmL)IZJp64F!kgB zFIG8)9e9CFdu7gQDk?jTyB9t<(is@W(_F7j%HO4J4hjU`XFu+sb-(9Mt#E8AD5%WO zKk;3@3!F9$+&VsHv?$5VEvcz#C}9=ulma0T%!KR?pI&z+j_H@uVRmw2VsBxBwokU< zX3-T8=PR;}44!UcK&INsNzbqreX>~Yb9XHRe$~^kna1)@0$GGeR-kp9XX1#hspi$v zN%jZYPY3Lke&{^jZu|iBguR>T5w}d!;ZNT%N=)hko>fv@-1=}?PLHeDeZ7%l>avyj zw0-7{_c3uzu(WirKbb8?_Lt}4#eoXFNXr)G)3tWTH;y=k7f|4QF)&OAfq?&UG`=B^ zFmde)Grj)BmR10qY38FPMMVac-)kBg4wX%BhYD2ZZRFsRV8{7}^?0DeM)MdfcaMphD!+I;z$I}B3kI7z3ggU3a9Rg5yrlsvVHy!kchSGq7 z)EV_OHQ&B{yPNEbA0_8)Ec(mI8=Rv;FCi!hMH%r3gK-I^T1OWnav8tE4ljKC#MS6E zw+|_F@R6Yd))>adojsIoBU|Ix{>S0#0?@eT;%YP`5Iv)q4)Gi*hdp{1X{a>Vu4?qv z$2iB2W8G$5AzuV?Wl&%*VAVAG6`bGi@LzZ3_y$2i5YRY|e!>P@I7m+;-!qyZ0rjJI7H=EIyO|O&Yuc#QrE+*Q+f%6lBmVW~!Ox`V@?yP5;|Zbv@@VVXSs=_o4bb z0X?bIUh2YjqA66+7zIbFYvqffFf9_=2x|pGgftaO^dLJ)C`2%k!?vvcvND0$0*M{P zs@eBFv-8|`yTu1aI28iO-Cep@?wwcZ2r6VwoL@i}vR)=6e5|N=0e;JfKfzmCRGVdc5R1t3;j`}pzNciHzA*q)I2YefKUI62Z+oz`w5)D!K3`9Wg|W@nIufB5IEwIi zc2@2KK?}J}cpsJE7W|8v|87iiS>q^Yv?cdL>agF=Z#)ja-6Eoe423K20$ai;>wn3} zjRU`LHwlA%8sKxm#mif%i$*KD7Oh>Yn(e)rZ(r81@rkAE4)7!75f`5w8*^E3?Pu#Z zkIwWx+yxx$B*jg=u2*}2z^QNdbq8oxhf8i)H zQoFO8U-5Jp&J7y|a&NN(A8YK(ZqBVz{O@7ecdvDIo$c-Wf&22}GP!CJTWiUD{rtJ6 zZpkMT!E8V6)Vz`0#NM!O;kj^f5f=wUI4(Ry#1?dP9rI3iT8@sB$!xFcmTsTQnoncuhIa@`0Z;F24!E*O_wJHZ6Cvq{C108tAL_}8BSp$9SsAYtY! z=bnlKf%Jq5eFydG`-n=hXSmW*8ENHDK_Q>yR#(|vd%j0hx?w=gjLcAo679JpNZts) zl|TZWs|Zq{Djqdhnm(-NEElh1OjOqI(p81ihhU)6Ai+Dk9@SK$UZ@7k33Et+-Y`^) z5LUUp`8L6mT#1i`jszn5^r`T6oB*RUwTRjiB{8>AmKrI7!MnreM@mM;M9%)G%^b*Q zhFF-K^aRd5pB4(ajvn$VT0#s@&nrL`o|%_tkyhZm)Eox6Pbyznn$RNp9Ijc!3U3l7 zsxQUD4+Fwt;M^AgB<6S1CcZ3j3h$x;ldo(_*g$+gN88V$qBr1GLRl)5Rfh^|O6;Ni zKP$pSf<&PG(Om^F!20_EM8eDzv90bqxv>G~N~)UApUXEj-FX<}g{W~{Qp16B{EqAO z!NGiZ`0E!?kq_?K?qjagMld;j{Q*u)@G5Y;@TvMAHt4opwPb%S2!j9;VxKP>0jIqz zBo^>5jg7nA-E;qz?%jduD{}Hvujb{Op({~}i_LTi@B5Q}lDwuW&SU%X(uZEPfSoh% zS3Ng?J_B~UQykJEDI+u3{n;=^#HnHV=xC4Zda5p9WLjYTf)o%2WE05UOb0%U<;r37 z_2oW1$W$35Yw7ChXeVL6jQNKgHJ^0fsxuzXSg6^hJq|zJlK_Y32yba^Eox0o(vJIO zvf<&}&v&6^cK~zWa()W<^`*PJpXTNehlc^+U_?Big$LN(ro*CV=7L6l(Iro90%%|4 z5qB=KQ*2)O60-h~0-s;SHW-`vi(`Yn>G9e;CK6i|*!s3K=0fH_yxd6qA{{Dw>eM0( zxKv;(vRm zQ)le5OC5p}va)J3C$t8pp@OTn9Hr+1oMG z#2rs)nQ!vxfP*2`jy8j79v2sUM5gykz=)fT3^%;DwXJ5=e4FSUSTCgVgFIp>;O=vb zg-m3#iJGb^0o-N2HWLttloPK>OAakB$w!|6x@Vwhctz|Grq7O$wY@j zgTnP12mqvm`0S@t-_ZB7Bca%jOT3P;@}@t(_42Z*Y63STW3u|U@c`ij zw`x;mW%GAh-pu?ur$y^n`3MLCAV~6<4VZ-o)=Ti#^x&K)gpi#Cu(SG z3pg){b8pj&t zM#;s~?c>_|Bu{(-3@E+t_-s+`+uTV?!?aAs@2_Fnj;q0{36h|o%~w^}NQ}bGrN3aE zBo(L|J^WFl8-APr324PE7#koQSP3MdP9@vwRz(`sCu&umdhrV?->5&De9AxT04h&*Av0BL$ zH@a%yy9-{*;LyRPkCT(p>bO4#b%A!m0>Lm7M*RMMWncp4`~Fr_ zdAb4M;t{~sASXUvooa;!&iE-_$?Ahh_0`$o^CN=tvND$=RoW&weNR>^mE%q?MHr z$lRQ|%$C%`p^gi#rt&+5)wc9UB#yMT7Z-~3kK9~LXyO~2#ZV>`d2s%CgLy?HS2zT~ z)|K;2$_27HmfXjCZZJR6rSzvH!hfye*7+cV6qu@5`)hLlnBK$*nOS5&ngPs8Tie~t z49GQ?c9y?c%}NKQhRGcY71-KQ$GuZ&mocC}d7+LN+mM!w1pM8M1?13}%XA zuJQ2qmXR|GAc+@iu4x}}^=PoMF?}g}3!u1cx5U zOJeDu{^crs$>Wi6G9rN#)4&@=g0d8;Ae*Iauk+#bF>mFLNN(ZjNz_P(@hnnuw$ftr z#39%yWy}@UaP-eqn)d>SAs_hDC4kVqLXS3^1Mn;awHFlN60dc1tnQUHiIKl-FnE?9 z4Kra)6BB#ZST;D329*tycD*_65?H=J0dS$w(X%n-a!-$o%k%SP;B^d(9I_pniXDmag!0y+RrK8!>-ZSRn{+-YhksgApm@6r?P&*H)P1qA#~6Q0!d z^`|B$fwE<}ySsb7MyfKq&{Zg9^KXx?w|%c)cc-w~pWU@23KLrlp>Abxy#ZG^0{db` zg<|Ki=+#0ow%B=F%o-I6R5oVKg`KsZf`nw;;~}W(>g0q8WJarDKF<&J^2}4i|Cp$* z!~kSrqa>d>p<90D=AXDIFmJWBned6bXKG)yE$^LdA2NhN3N&oTtBLb;P5pVGO^t6k zyP2$F*3Pf6NyBZf608zOab9-2ocz7*?(U0=D^g8wbWDe$EcWFYm4QLWJ{LP@_|o4P z6~-0BorPbD$tFboD|>nRv!H_@uNy2!9X#!HWj(TmM>7dk%tBCz2CNhOQ?J#_s+sUh zc?h$O=WkAb8h`&3Q>baP#XkYkPuOBB!+kIOtFR-Z3r{GVE3t~Fcj==K1@=d|YN9C> zAFRrw6-Fd5GC8>K_hYB*D|&p)Es7*|xEUq{b$$Yv*(jwYgFlW@YNY4FeX}ZhSN$e) zk0(HBy0}fU)N#!+HH3by{@9S?T@2l16j4x+@~|H63sS5k9Dx)mmy?0CA^GgZ#9l)b z8ga^CJnHx9)=!&5s0{4c%hwdY{OCn~HsB!(mhWXiepRxL^WuagTWKl*i@9a5Y#u-e z0eEIBJ8*LCh5Ls~d~{U!%H@FwkgCp;aN(mj>W3$eEz;ThMsi_x!FRE*7^dXQN<@M+cXM6UnQ#@M$*l1ExrY5xJY7Yce zrm<;_ae&BGla_3kXb4Pw9M)d86{C+Xex0YKtx_Gl5?J3R49t zrvPMpdgl*BVCUny+S*A?y=e#pU|!t4_Ci}5?Ri9t##%F}yfwiBTPR9? z*eau9n1f@f;m>c@=F|wSk@aq<2o!bfO_KKzP?@n3B7?pb90bhXdjU1_^ubcSiuDYb&R(OAu`!UuPIsH*bkB$^@;S`Ei;n2* z&{PwvKp-+;{AbQDIxGc<*ZeOjJz}fuKaB7?-kjX#l~zy+~Ib zH)y$C2cCsjQv-xfqd*FL4a;uY0cwjTjnzpSbJGHx;^Ga!xB7==Cb6W*QRfh_5C%T% zW&2*QW&pkpV6B!otJnI08y>ZzbK!cX{fSs zaL~}6)X%2_rZV7p4aA=UOV^`7rd(T6a$ZvPrr~lf5F8egr~wTsD+W>>)i>SGke+ng ze0rD)Gt_km9c_yM404acdp+x4tvEUP^`iQVn6VGdCn5)e+A7y4b;p5lYJhSFE{Hp%~^?RNaHIn?f68bNQ3unEM z$sFW|F(KK3!VK#30}5@(XZ0w@07Tn6tC9e#RDqJMgiT9lG(4~Nn0Vzxh6t!Nf$6{duA_afMV) zd|sf2?Di70r6(1F0nag<8DNS4OIB`?Nm14R=>n=oL7UpCom3%XQ&e#ZFMB6IIUy(g zWIjl6%BE|tHeHJh84ykgN6B8m`s}63QWlK8b3au=M**5(A9$Oj=MhG6eEQ5v6oVe$ z_|?&?xzl@6Fp8{KjLvH~m4ztec+u~S0X>Ww`n|HUq=eOg)j<0AIpzH_0MVs<0;+U| zbsrXdkB$zT&nSVjG#W_QIy-3qR0k-OpoiU84YvyNC8=wNo#~5xbdi_$416+kbi8n^ z^WPm)9=^KscXY%M+;ZXtZ7pz$0%-V@wyv>(iOFqEIneLP*GAB&5&+%Ds|5q)!{`!v z3p9%KSZ_P9P2A>e86LOg+q^bYQ#Lp5i9+nt8m$MzwU~`{b^SZBD8=sf$N5x6fHHyC+qB9`z-x(&!v7pQe08?5=4{C~5OAo> zuK)WfDptt4cY9k_Rn^Mhzh-S@`SB9SBYpOLZScdY>ztdO9-jk9;VnR0f}Q*g>$NPX ztgJ35aC324&+-jOP@pB)aGd`JF`BMvZ4LH4ycv{u7%zMy;jgW!DZ`x#WbqsCNPAI{NXW*pS#i zrjdM@J)V@{Qs$-nL0bN3QLDRMNze;71}%uxWV=6E;Q%>X(WO3Wp||#pRv(b4)%)5wat-cC}*1)3qL5T2*V7P10iRkGJ z3Q_cnW>XoDpDykz;(ZbhxQyj5?Kck`!l zM;&Bnjt=l)daJpX!0~u_Qq=L5yRP8sP?OjK{7yRzKz0xJdD92$>YR`E_X7YKwrRi( zkQj2(OM|0NoCEKcGpaDyz0~aOm6er4*97P5JONAZ0}C4#3Nk0Y9ClV9QajMsbKoaPb)C?k-~aENeUr z5-WNt>+QV{Y*wjR$FQMAKNJuMN}JLjxM%p}z4nO}pW4i`VTt0J8t>I#^xfB&2bns_q{-9_srF@!ACqEiV97r;C?`1iQTwy z8(4IIGqABfivzVM(esffYB;e)%-RoTSF#U(0`TOQ-hbwzc=;S32$%3aqV)GE0l1fB z^t&80ENrz+H`M5pYcIF8?y}n2+s#cSC}^vs+}V9^O1#@zDCwo0Ku8DrjTOMA^PHU? zc-N>_YC=BvhM=cO6P%a2@gXE?tl$eG;bW^-iTDrFJ}=$AzS)>wh}0d^Kq!Q&-d>${ z6YqpdNJ$dBiGF?xLjXn-UzH4w@LPkZ{utiR_;jj|C}#?_#VjdS{3V}|OBxHREMO@A zCWIOSW~Z(R0Izy^8jSw*<;H*_=BRsDjl^wpQ#ap%1qog&U`*e3dlnQaS|NqWx}BHo zytR|hV-q*^rXIxtWe0A*S|8ANeVknF3)g4&*Hy4GGV*`qBFb#7_kF)>^Yq}ete~WM zY|?;wXdyt8Y=Btnm($q=6$NW!sl6)x;p7SS(cT`-zt$5XIsH8Tr1VK8*O7DEAFU;z@m$ zRYGv8Zl@8dV=vLhGOH?Y!0bOH1Eafs*ygu6j;iL_&EM_q`dmhUYsEV#owGgUKw=Tl z2KZUt`n{%4NdNj(3%`p~G%Un6jUGO-$EOA4C!d^3g3|wNU)^B=XoM2|c(N5vNQ=oc zXhyH)axWGXe(Zy#su?8*)G2YKldYa3KbnnDqN8%BF08NPZ&Bl;<3}}p7#<9j1b3Oo zxd>Y7f8b8--8$>aEn>73=0pZnRUQh!l}PC71qf6=t**_NKKS;wb_xQ% zhQe?BI$X)G1Yzu=6fNIrw=YOfyr@%H^peB4mNsA=Gme63{TodAVCa>_>%6%4)o=~c zScbKe#9fPNObb$gvzX9e`c{qr|OWSBSP{^cEk;y9C{blgV*iP`V_k zo9*d(YRcP5^mYAipAQKMU@jhAnW>P_Q=D7omi7ybqkU=0f*U(0(&Dyrs7mgb!2?L~ z15H@WYgFXaJ?t;q8re-)$2>KfwxGOV^;bFwnO*xuAuA_OzBk_AyVXQ3-Q}f6BjCmO zb3u@d4=NT<3eCQZtD}ixT4>Pv71>E1yw3D&{~7j8o57_k>^xo_-InbM3$j>S9%S)F zd$TYQh`hh{v}-`iyl0Qhv&>dat(!nQ^9airyjts);qlw(z|5I4f(2bggm0m65L3!5 z+I5JFe*6m6Gq>rEE(AgicPTvd3jgf5X>B|k0t@H*fQRzInQ#~r|7*aD?muGRqqzhO zT_sq~s`zWR4w0hc?*0rc!iq($DHuIB`5%fFX*l=()MrmTm@1`0G9cWro(0-4eP59l z9I$5%@+=RscP#ey;n%xwcev7jM95jh*;?z>(jTXcj?mzd5LOPN6|jfJ-~^F%DcPVB zY=(z*bEUkICk#zr7#erq62kdc<-AU=_v4jl>+v{8V@q!l%%+Ag|bLMEz7vy zqk9jX{H6xkBUdHgW<4TId(n5k*ms`m#9u+Gmztc^<58&sd!TAdnNPXfKDshVr3FiG z>t7hYqMN)~gI&t13ln5tVa|yZ%$97=HjTAv(cbu-)M z4k}_paB63Lo5i=`a2p32let!C#4fxqPlRJ5^JphtV4nzM4zo?I-E?=h^)EIs`&yQ| zSuu~?#*Fy;?NplA3kBQAIu+i_!$ujq-LCHXDrryngWvMczx38&OJ@vdSgjsf>PH>T zx_hqf##Usf4}-`tOfim{y8Mw3d13cG3fzf!ge6(9zmD^=r>(2}`d_Wz$AgilXt?!d zx-gm2d?CkB>aZ(@mscw{cu5HA>2+;~?Yq>^&_YOfaA;ZiGa`-G1_LR$km3W`$3$eY z6t|=df~svB6j{^Vp>pW8!;r8IA9bnoL0BGkG2?)V8CB(#glB6bmPNNtKV2s#IHsiQ zZ_53#;?a8)1nuTl?Y1IUAGAwC@JATxVCuYQ9>~86X3^KUD<}qK1}$>hNPE2Au9|4XHvhY z93*@hL~rkv+nd5Ca#iWJ*kgJYzlP&%?S2AjMR$Ze-S=giTG=3?JhF_NH(`7cVLhJg zr8Fwli3CSZ_dtMVM+$RY{q5}Mljj?q$C$?HUVi`WeV&|m(UN)gSM*979NLYMis%p9 z@9rW!jq=tija5P77-Wtp`ZBMu8pBCxzW^=?ROD)3zd5>FL1oEA83bQYCvnU!Gty#M zw#XB|Bf^X6)u1B2%OiTP1KnCk$+P62?wDss%Y_f|Oj3S@psu7_cbM3|$l`|Yxv1VE z!ZOPk#I!Tj40Q#lxf=`0gF0>UFt0{Rvbr)nD&e|3@*qzs=06==+C{2of;XhJg7n!% zL8#fNafn30b1nGLy`5`mjj-IAHF8QcZSeItltU!QD1nqJ>rrV>3j#O&W0+W%S_VpV zfOT2wOK6gk2MIYG3^}t{sxZl&eCHE_;1M+r&S0kSbONhkU9+UV_HsU15lyKfQ-ujj z2plZWI=QKg3|^k|O{Yr}u3LepNg4{_j*Ur2EIj?ZXxa_J+!wkta za&xf_=NzVKA?oto>%Mn&Hh!X&Rrm472#U~r5{(qBoD*7XF*xyY0A9Q$^@Zh{q>Gmr zM4XhI`Z~I%hW19o$P~Yq}6lKW&nCFhdb4OthD>v>G7#OiWyy z_$(vD=I(XUOtMYYOG)}a_f*raiVc_tAqREp*eH%q`|A#qzNU;$uIbg8)Vw9ffmW|i zLgZF7;wZzi;dxKbts&5tF#OBA-^)gO(lo?y=dZ6oURr;cv@VGFBdO6;V7S z`Tbo_r^NwI&zqVkF?g_N0?-dQM-jn7g$n=18L0^Lut+Nw99=k$;)u5wx9p1&Q>NV> z5*y(NNXhf67 zPVSmGpyhmJ3Ib&vetrz06H3ZTfMOyr9ApM`~8ixty#Y8hAJY z3K6|`C$&hs1v2t&vy5-weq_CuWG<0fLx$7fD%6PO#XjmJ_yzyQqAAGtE2a~l9iz}2 z*0*vfzTNP;wI5+MtGl$AbtY!X-Mvr2fPoa-OA?IjMXr||=tP$g_HhjURwPHk7B&mh z;R-!(x?;A7>_d}o$$;toH|p>}x{{3mA1aV?WsE@97}zw^@d~fU-TF=zKlXP+FU2SL zRlVXs3^m<%yF(@l3stHZYRsF(pFaqZ`nCEd1@(m;H31HvUGckJ;&O>T9tG_@*(>V|3XaT6cjug%V53loK#I`wYlk6qe?9jti89$OfMCDsXxJg>b(_Ci9!EJ{ zXTQ|Lue)8+K&)M{la(<@w_q=_pU<#up~0Nsk=o94!mRLlm3RXptC?)37s;tJhT}K|PB}L>YUi1U|># z=`=Ib>4F~8D#`7k@@ZZ_hCK>}ler{xeZnc#pbw$)Py?EJAKu_8bFftnJ7nJD;*m__ zDRhDqY?=icthpc9>)wh(!9r(j2)4yUIqk6gpj{Zshw;=X9>piww6~j6`D|6M$n-Fm zu<{Bqvi!f*1Er6GEaQ2=3L(2lWiZS_ECenXUdqzpn7}YJQ4#UGGRKNch3>A(M=jsr z$vFLLfw%TbZyuOSom79f&bBqBpIE&PtRi1Qtx%Wt&0M4ZId-`@Hi{b~K>anZ4owyCwdMW)S#KUm=yn-ut2g>7`3JOC(f&Yid6GptvbobfU)biNhDGm{QFKFWdcL-45UYIlXCuHimFkl6V*@jH$Z0WzAdS;i%EOf)0mHbeACu1(Dv# zX7h4Cd~opyLtVZvB^Iih{X1}+UmF%AXr-=Cg?C6X(Ccua9l(ToRG5Ram1Zur^Ur` z*I`Vi7qb%=OBC*no6lNR1%tQQTom%-+XlWa;nQL@xtwp6=u>6?xJT-w<`NL}GN3)w zh+c3si_a=k_1!1NU3HIw&Ab~8y+vhqS%KC3JS(`zW`}+s>1d*U$MeTzc|9`_$rECa zq6s{m8m;QfQ{gZUV1DV9nD|^rMffzC)ahKc(dO^!9_z4yV4`X)i8`rg$DN;8TocRA zY(cRnt+(($N8U_Oz4QyI&MC1U`F-x!pg{)fE5LD(V3(kjo=e8+!z&!4Nt|N`RK;Az zs0a6ln13cdOj@}bgs?N*Y@FZ_P}sq`@^<{f@E9^^7B3`KwtC(@xqec2?gK};29;gG zfuJajt9^Otf+5y($Ceg}XHzSxd^rL}nw1nx9&M6^KNMZa5(XXqI(%px5jpL6)i=}H z`15S-VJU;(aq@0YHiH&5J>?Cb)U47mBD6?t_Os{P#H=WIhxwC!S=(B-#s0o?=B!GY zzGC5djrUKkAv6du;LarA)*=^-r$J!*jWpkfwx4vHL#Fe(5C)DCD3@eO0g?WHQ9>JU zntZcVvGW9^1VQ-hc4fi_QxCNpIi;I#EsA9()*s~Gv4i411|G|6^5$hV@H$GJhKeS} zpS$+vFMIV(3t@t$n|D{DR?*3D2uA+!!2lPv{{{!Rki0>_kcv&+g*$979}y5EKU;?S zedcx`>S73<@~%p&if?JiJ)iu%X;rm6zPM>c&ZBBix=^`PrA__&bR+Ron;?W2J0)BHI`#GQ45w!hSk5ufE2un*Dcjtmv#J_p7J{L*kI{hFweM z4c(MwBj^w??T!C)fJw~PJCUJ! zC2fHd%2oXPIU_0^3DyL#2_`f*`48;W;s(Q*=2FT`DN2l~Sf*0os8_=AuRaq6ABSD7 z4=DGa%6o0SJJ;^Vy#YVnPCrmUV#JqlAOUs+{+PvcA?L0<3Z`gl32ScUYR7 zh22z_d-JCHIXwfCwzajcqly=CD=ycx#u^6`k+@?t?{h@5s47h8_fmfb6SqHyjvhX$i!#Cx*@vOvzQR{-K=!d*}ckoNP3aQDwTyumV z2sXj+TuA0Yq0`wH3EL+<#Xg&p1hmaXBSYOv<90qz$VOB45*ASt?=iPtQ&m5csCoTr+f9o0Q#f&4a`0G@iCDIm_f5 zj=Da+eja0tnX z(ftbU>DyJ+dCXCheE9PE?fT^IWofTb*NS=g%F9e?JRHf2CnsLXL6Cq7f`Cuoq@Qc@ ze6wKzcMxeoh?3_jt*ktnp4g7)0;PMG!?Osb1ynqm^WbiMorQnxC$OMC{k!0&eUtwcOnOfZgxD zsb*N&v|R&a@m=pZ7Apjx_U-MlZ9nqdEvj5JH7|(yn+vh)eH_<8&oEo*gRy8Jqo4Dz zDXOw9z+#lA(Iptb8K<&!&M^1Qa&yDL^`FH}H{)Ma&}x$bL%^;~b2#XATEw>ZWs7vU ztv{{xJJ~dmk_`zmbX>mv%y%M|;9hK}IiE0ER|W>-Y;cJhqY<`HA){L%Q?P$iUn@yh z@#iF;W#Sd%_1-t#Zu-wZw*$)pnc%<+cfM5v$Hlq83{6tKTVxLsckFNN4pY+vNI-BDM)0?HGXNC#Qxd%v~W@n>7W_)e4=>=bfBkWjiYYeF4w2FRY8$29uDfng(#kRnzY z_(BisG+c&(@%CG$=B-)%mezY_;n zapXgH-!yeWg5ia3U>K*WMk`z3a+OqAOtp8Q*jx?)oE0C$5irQ>oiE-#!xv%r&Zj5d z&9;l{_BV>VuT&f5(D!Pj@9CxjZmd}7!La}P{?OjFAxYB|uyv9z+G}nJ-o)MfV=$B} z9GaIOMtQ@MS+ZxG{X^zG76g8u)$y=YTh*hFHkA|0`$xl)YcUR$E!dKhZD2!wwo!hu zIlL^K-i+mz%}I67)&R8vOe+^Ff~V{5z(${%d=WtgInBTx#gKQaY6(Dyj3NAxfRDd` z9x~(UC%aQS`@xlI#`911M;KC|8~iH=vlB1zxaaVtC}8lJP4($*eCyN$)MsG)P=o>9 z1dVPs4be`~uodVwtwg>f?9-BaLyN`o>OYwMtW65l@)9H2fZ-Mw5v<@W!6a}$Ly!!) zz+_HBzZ`y(MCsopIFK0!sT`8Q?x2_C#{M$Ipv>a2O46y5rWy>%=5n#NWz2yGgCTF5 zG1J@S?EOvNOzLa=pok>_%;pPS!fx*mr6&IE_qwMlFPzKoOc}3UJZb937AOR&*AP<; z52?bC=DLcEE!MX{V}Oz~6P81?#R9_{q++A-IS{}%_UY?qJSP#7-O{#uewUE`!Qc<$lygzJjSM2|Llmpm`ZMW*+!S0?v z3)VpM0f2^tUk~8T;eS>5&VIZfT^h&36HOhx8pkXpb^E=z)AZ`D_nWT>$yc-hsM=~+ zrb$T+@9CioTTDNw+4ml^7&WYaV#}WBRhna5gjX;qN_pw@PdFvmXTT4-lfmJTqAdBn zWK>$~yNJ4*)l>c7@YT1QR7!NLyuLgj?TMFa8hNIELVxJ{F5bAo?Vz6>Xw;{HrrV3g zOb;9ujEf25#(X4}q4uM>vi#WYh3+v0k*Mi%353Z6u+1**T5(bpSMcp@Z?|o~zh>q& z8-61}05y_T;pKJhbfE3D2T&}Ok(jo>T}J%_YT@B3Aqp43R7i*{sHyQ?8)iYhaQZ^b zFMaaWsIs!GOk@bxk~eGEZ$CrMZ?j5nD{y^DS?=#y>m`z$q@yue``E~NXCb?Z8*@ze}9r_Z7T7dU~Mf1p={nT_D2Go#>nXi+R1k-y^ ziy(#1!~>L+k=q{}xN&0{Xxwrp9;3d%LlX`7Pp`S7r#+7{#GS=St^ta@&T(of)hys_ zRHd!taeuO&X|+Yy66lv51uPe(O-&a;!t}#Fx3eO9D8J;AYzr=B&eZKkY8pSCamqo# z0>1Oix9qgegSB>-gV}d2OFRtKEQ$*oPS8%P+6vFze4jP6t1d}O%XfgF_-TLi;0k(j}H;-rfUj(>$H|;nbLZ3 zM(Zt|{pU}4y1H1py4>Fd%a#^A048*f={7$Q&?y-NZR(t;sSCijr6nbQTXW(2_j^%1 zd}8qfQIKyKJJ=g zMxEi!(2akI+4>G|k}X!|De_AD=s3?HL)SicB`q~d^)QV~Vz>De02W}tBv*Y`@4>+0lO0ZZDCL`F3J}bOS zU7C?GBm09XNusSpv=hl0@2DaHZiE*)t_u#ix zb>1wG9JcIyvy3yam>Iq_%3YjYIk-lgmDMsd6boQH zI4&on?cKOlVp^2%yCMPzy00Uk{J!EU^7y0&1C83f#d2dgt+uA)Q0>y4LRbGw3C9J% zgCC~EZL8|mg{su4mH$h_F8t`TejNjvCRN_vnXHbu2DvO#b6or$oe=Ykp<_wD&ATuz z>$d1xbbW>sBM8LzKSpfEm@x$C_kdvySaW#t3 z`E=4lIrOoiX=PDvt{4T86xmZ9w(se@KuOC*aps8Ylzi)sHAz<|gWhbc6LIjCVWW~$ zQ$0M%9=~FpF#_9vn<=Rh(mLLgjc2CghSAbdl9JW+tCy)$rri)X7(94a{n6S7E9=J) zu0F$`S?{hsxqy{}r!!XuI%M^%7mN}3zF|vk$8Gg>2XQ?3?#gTH*77eTrKKJ-EJ6h& z!|t~Pg`W(y?8iyNW;JX?X>8joYWZBw=*+nLxjqcGu+epgQuukV00O|y*))4R6+WU4w#kT3{%K3~dIK=2N5O4isgT7IizzORD=rC57;kbv%8 zM`vMJim9h4u+gL2@mH5uy=}1;THaPh%`fV##okHP`)Sce^@WH9x#o^aKbFQHaK0{^XxN&`j$5G#-gZF2phP$l1 zSU&h986fa$roQFJ%T{@ADSs5eEJWq~cwe{ys3da%m(}#5V(c*}8#L?w$yr6Er>C1; zL+-4TQbS*GAZ;N4bvVEOj(0O%*$1LhI-1=Hm>(VW2fkwO&8h)lV+;cpM&Hb6z;cg{ z?ow(B`%Z7!$qTIO`J% z$CzUl+6lh2`rl8yVh99y_cDUF!9t0leXZ)>^CGtO(my_L4pF=G$*8uSmEhY(|H0F& z2z=sHBz2!J`mgL+J1Aq(YOTljabc`7+uOnDlSI4SlP_xqGT9$~6?)iw>$Ix4+6sp# zB^rh0Mw2rtiExp++7#S8(a9;wmQ{8Fbl@{}N8(S2h{AsnA%UOZ(!L}}e(3!hR0)Vv zWPL)9>)kX;2+C?$T9!XH+&8)S){D1 z?d_Go0F%BRzwxBw3|WCL%(aJET=w+WCjeirgJ9Rj6s-SrIZ#zcQzF;t=V#?%0?Q6p z2?)VJDi5of3bvLM0u*e*=|ScFAWlx16cxIM^`TelySH$a@cS16E+3Mh8}BzTQLbXn za=%sxp@sAwQDUSn_0F($+VS}14l5;?%0jurMsLMYeqR65Y3EjG6E_*zN}%!x-`i~Q zWdLS{?e{(O;4DZI>dB2lNv?fK3`D64rEjmV8?_w20pTPy3Uui41q^7G`|!FkpZIJm zjwMq(2fbyx`9e)j%kGq|S-_vE)TLdvwQsJR^THjGf~yk4gX-aqVqV{qX|p`1BQfy9 zeDj#Ap&e(3a=e(d1HbMxrlv- z_4JBI!SY7Ujg0*H^wimF!p}?p>CKX=P=)Wh-v0MSk zY4jG5=T8$}S6Muww$3yMeg@Ftx3$1QUFVvt?2xFaM_bCTp5MpIcjAT)^F?7~@aOU$ z;%JMM>utz!p<&T{9F)G6mW_efoLYv$ z5n*sW8-G>k2txF;;~~&ytHd_jr@xCfXCE{s{@WubOG%cRl$yH!o_%inj0m_rIjPt& zFaS{`s7g=}nLQo1#&B+H5aUoO-DTI&l-~6a%i!lCN={CuM13>48j4$`&(#gHO!`FI zdNP~y5RI-d_5WPo=C8+^@a%iCDCjdK*9z!LM#hcSU!j`BI1;vJXCFb7@)zt3i~C1b z&T5hP&}&dwCV$J*P63>PzK46?W20#&`1K#$06xB(sXUHz-=rkdwWV17HSkG3_iTS02+&Qf7TyThitZpEKOB+|Lfg^mPw?nuebT&&T=l+q zswc$3&VCso6?{ogNZ4&DqRezRnV6`Y-_yMKojhKuM}QlEMjs!C&H1^XRrTr7ni zeFO%fn6AFfIRCdDI3!tMf1#k+*_QVev%Wic;E4PNf+W&AMM+2u*d&1`@)t-V{ajx^ z@UHci_Botwi%`7B)RRt5$0g7W&~?=}oYt3>Q2?{~ZEEU1tj=joyzM9l1g~w6zj#8g zD>nT7L*|daUISKcf_m(?wS7=>u@s!i0K&$W0%tG})m2ao%=he|-na=PoqCgExGRns z?}?87p;tIOGgH0g$VNp;X;MHW#`SU?3okEo#f;{z6?K@uMX`8+x#hgoRy;J2fl`hX zYw(HEA*(9O1!6x%-@NQrjJ6bm)9z6}W#kh`A}uf*Zdf~OjlfIZQsR}@iHO9;$aROY2% zL22X-5kaplupIMe&n&#v2%+aAr@8^%-rUI@bSG!B6V}k5*`;US}cg?~J5Th7GI?~Fg4ovnrg4~0<*^;H5i`caH^jpV(l z);TJ|5RfGn%cybUpd;Y~CUcM^7^nupULjW|G0H{O3|xH*bW6}*?82gkg~7;qUeC4}WJOC%?M2j&k=xwHBIvMP@R z*7R$uMf0BZjihk3KQ9Xf^aQZhBSc{B=-H;F)A+v~g58`8eiMHgaS;$|#4}Arhp8V( z+rtm&5kYVT41)D+tJ0&=hoSU++Wwn~a=?ht-MrVu8raYcd-zO*p9Yuei-vt?%;9Rc zr_~m22plD{QZ5Wb5Vv<_VxG@Nn_wBpwL#190JT@A8 zg55Brcp*6|LU6jM>ke!oMY4f0Js>}wKW9!{Z!<062Id7%L&Ny>CnF)JNL4oBz`yWd zgCj2C>)&R>9x|QAn1_uZ)Bt`+B=P&3q0)^TGs{3Y#VRE%mg~rS{y+M|sWTZ)GKaI8f7!!FpBX%WB%(D%Q=xw;0svy{-Q}iz zey)JA;s_U$J({tb#WibC7xnW+!!_0<2i5JZy3~ba#M0=t6#&5YMH7NP%*3)uEHurN zp`fDcsqdn|;cW7coFbAi_UEKxzUHi{Td%}TlN95@X8lp^U-*erFRnVXyZ`th$soYz3=VyG5v*LtLB*UZ7MAuA2ZAd^yk-;wEkiFK-=gUc1h#&| z7ZE>KRKY-S{;f|6mxyhRAUM-h3P4g^-sR|QQMuvE8pNA*`Llpo zl|oSNkPfkCCDxHN_RuQt07uwC)1!!(Yh%lf=if^&e*^cQ_{QcAvG#Ia-%>3*?|fOX zCXV5b@$1i?OnBb>_8HHa2{^&Y)#^j@Q_-a8RNbXi37 zXXX9*J=gb-8D_3)cFs9-&V9eieIitp-h!~nu>k-8Nba4q8UTQ41pqt>ef$V@XU#69 z8-NA?$Vp3Rcr6{=zD=Z;OL&Ei9Yq`RSri)lX#_DMX#j}phnA&SzFZ-H1WyzVlx zscCTdCpdP%$PL#``2Fd9cI7%cz%G#*3iSmf1SBK%*6%Y)FY4o|=hT#kmto=q;P=!p zBNq7A9UCQon%Yi|!x8*zb{FUR@sN-Mn8pGYgn| zDnK(BSY?DkW>+=ezKdKwOMQ1Yo6U?pg>QDkj!=$68~Wd|NF74gj6pVW9{RI$KCb%~ zk+&w2lJV2%GHpp=23pE58;pCDcau>?(^5ldCeF@}dak===(y_6qi&IkT7n9LtZn() z@)Yj`X!PqS=;paB`oc-@Kw-KW>bYW6`~J_1rqP=)$ned?=Q)Jx50 zlH=QmSF=fi))axn=A<1 z-{wV;-O&D=&={mhgf5_4R_%@YrqPTj4(5{KNeXnu3Y~~tSrYV^mu4og%^@L21^wZu z5i`efa%DHlB|tDg{cUz@d&FNCHbFhFafU6x&sTReqcGy*_u*@?JrgmZ2GzyUl{vVr z=_BWs*${8wZys~yWin}3uwmm7mEm?0TP{$=#HO%VgrhTv2l9tCiIRMTsyBi6`$6u(wH&BN zvaz-0rKW#@ggd;YoBItOa9l&KA~*AvaVAg&T@qx6z@xvHCj{{=*)m)n7vH>BcujL% zKr5Eh;T@EwcRV6`w0&G#?J4_6dKaGuF8zC!6TNqWL%#C`ttI05=5kbB7B4Hm$We4d z_fZmB+ELOdu4pqtm!d+bR3ZXoVdFWfc=-x%C9^DwOJK9SjWecd1{@+W2$gu+^X>ju zMztSj{NgIKHU}%jvTVd(IXqSCJ1u3BIfGQk~THX6HOjBw!o9S)feDwKn0>^OL9G=@iW1dH{ zH6j`vPp1tV^QeukT!I%N>>=bTp^v;*Ka$7ID~d1{H?8+qQKENu*NmNqT(D{xKr$6^G_kQ_-u2i-b^UyWx9(VgO zso-#5-J?I;Xeo`|U02}cMd&wMalA?v#h{;zmvX2T|8pMgsm8Aw7xEAYHa0P?=x%iK zFS#Nrjm*p6k(*^r+TInuh4LP}D(J~9=Ql~+7W9Tujn#AS)0Mkwsi$eLTQl^0G0%Wzg<;OC9vQ$BBKs8aBSKkaY} zbu%d$l+~QjcAyljYZV&K<%2hW>;DrA$)C7^gfZZARc-tLE0Ec-Zuuc%HT%NH81rb- z)_(#+6fqr3|He;&=Cu8rNHPtHjtbn+vF8(Sk{sZ9$kT-Cf{_^&x(eu4!d}Yv9gg)_ z?rf!s_o1n24(%TyYfafWe?IE;fu){se{!GV(_jv%`H*=!LK3(N8DkmM{yb?oE9_m+ z&EdmD68K-+eHO?qDDAMBs*uyk98q1+7ax>jo|{0mmiO`NbWBsav|GAerVNW-yoZQ* z3)UnLLAdI~YQw^|6zr!L9t4qym0J1gJfq$@6L)V8E6$ri%|DpNqdffibgG6) zvS~}~nM?f?7;a&9PJJe%$^^t(=@LPb{v%Cy*`no)6TzGN-`Hn^eDya;N^ueUTTQNE zRe|afczx>_+G!a=7dY|~8Q{&}dYlUZWh%a_{G50Pm8@oaW(r)`1Z(g1WMHcWh9)20 z!uC!`jaSXmac_T3+}`0x&2*j{x;($Oy5SRf{#-Y7*GA{rJWQy%sk359w$rcP8mJizw$0V-|<*x;t55hWU4vE3ZB+qg|E?!)pi5%d#BmmdJ)= zrgdZIfNSv3cy6_QdoF(xQMG=6R|t-1nnP|q0+Pv9c@TD!m-q2Jkb!Sm@3pzMEIOv|7M`gYE&k>$bHoFWTJtvZ_f4H^TapPGiX@I*E?Oas zk;~^T^{^rhz&f7|+>0=MDF*&^744vcTYHvI^bEfmtu*TYt^USGl&mc|s|GtGEfr~( zU1?`pTT8FkFb#}EUW^*HJac560+ zDTYV?tjno5#btW(`meNfibo^(NOQfiAJbSK8S0w)RygJQbv!tAm5z1HtASIXvRiN= z1%5pm-T#Yt-(SS%uw2Jn+4qkA><_Vd)9{<5!hxy@65uW66G^@5^FIKy(`Ub%?q1?DXY+8i90LZc#_VCcP( z|8yS76^ym`z0oeDV-<$8^MXD0b*0@@cHIH#<`y>zvX808%lQ7>dxb;PS|uazr7 zd+09;H%~F{`Pom{4pSj21LtMc+^I(93#?&EedUqOJeUoPN^xlCDl~&o!C#AF=J6C-sG$o)$5`{xTb50>W7TJL7 z-R=z5`)45h|2iE!Dx1Bom}nHc2@qiV>Pr04z8-wYC?;lfTA^m>FtNq{T+h=Gysm~j zLoxFwdY7s4Ftc-hBEZ@0Yi7gn^I2tIfn2ueZSe9ZQ3;9Mj-|A+x{c(~$Elzhsh8OC zg6om;fqKWi@GXpUkmi*_GT#z*IW8`U-!SV{CB~*~Mh)&-3~R{NpmH?un-zR54vmW& zzo0ve^D~!;nf4U$#~i`$H2g#lrD~;&{?KUP@Jnu$*ljj90Tx8HAt^5Q=s38|4l&qi zMjIjnB6%4b;J*RWG&D!|#?mD)paaj+jRhtkdCZ`OUMhIe<2>XG7PvZoMWjRkeBds) zZdWys%&*wW+V?6h7{Vi|1H9lLh6*Cri&;_Bj|nw1ZeEiPBRR$c6K=|baG?cT>(J|6 zT{OxaJxj932fz4VP?Bgm`7#4u6^q;_JD)r9cDflGb82XxlR1`|z=C`*kvEch%TTDt zN+yKQ;}77qf5A8KxMH`{Mt@s_qY;R~>%6`NuOwxHLqRExl4K;@vx{i?orGcF=ok$^ zVN=1{S~!GXVli3;hGtI2s_LK1ijLrhCYg(?`XIP-Swj?;#8q%9No%QC_Rb5j$SNXP zCaFTcCwR^yw;ET=YE8fr=hYhM(snOe3w7+_C5PresU7)P_$#)7QjJtJTu^TP1SOOMRPL; zYhsPol;i#x`F}ChkpEw>LGk9c#1gdOVS$NSDxBKt;%A>A)({gg3zIhS|j`K;E7nYS%qc!GXGSRJah18x64h=7HS zdY5WJ%pljwTTlKa3?z^IYiN>{cm(j@>A8OvN3Tx_V}a&y#HiOTM>EH*g+ZqR3zn12 zEdlN=O_%Iqp~y^EO@l;ya=0sRVC;1%9$zi z=HrjV+00pa^;=-!|3$;VpZ{ac4{zz(t3K_U+vLMYdTUvKa;&pBxG=)8bAM-7(du)f z=!^i@YT{c&CguIbLM4JT)9A=e3&Tz~EpN4D9&%2^Xwd-7lA}xSN@443x5VPRnz8<{ zNXup@E0Zl*-2(EtzpY_UWHWG>m(F}F3DE?B$=-g~Nag1~SJ&N>oG*y3dDwoIfXq$R*^w{SxN^JEIAT|;kcq`Fv2ia9^>nc>z5 zc>KQS2COy0tD?V^2?EYGuJrH}nsgw@4$lU#iz=F=+-5tSl4oOi3Mo`tx`o%J(D-g- z6_K^cMHF3O6e~ZXUVH{fFQ)ncrSy`Vb2AiMK5BXkqSfWkcM=R+MPMmyU746!!6~ZR64$rEdT&9R zQ4X4^^;5-VY@h`z-zPd2vuDq`p8a`Nq4+sMqM&kBVtU1Sj*R@s_De98Y#Z=3CW;&W zM-eNT2f76S=5d({x5aMZ*?*lpPW#lv>Vr|B-RCC=^r}c7pGam)r`j*1v{6-CplWVn z#Kh!LU~^1XhEo2iZA&@sb1gFP5qol8H;fGJy**Bhm7Z}j+CgS%L>{Q5K*e|a{^!*z$( zc!$5ypQ#0>&8FW(D?Kl)ul&m;;WGlW}C(g2wt% zU+RI01vGAjgU08BN5}b5#8{pme!IVds|C{}=wP0z9*P}Js!yS6`NkMC((*CxRGpp6 zErNM`e7>w9CIy{z5!Mhz^r~JoO~?Mj%FPp-7(H4CM+?KQva|Wu`G+o+HaC_`JXKW< z_N4ZGTCq9>mHA96-zOj`Ir-*aNFI9n!1}{g`-6U4mAyDoV%9e@w}K1XLg6%2dSCxg zvGQwT8mi51Sr~jEQH;freeFomJZ@yJtt9Te7SEGls{$8eQNnfhu+-HxhY#qXv8)-) zYU)_qvfqBli!Z<+Vr$LW1;R6(K>vjlR7gTsr!gonGkm)hsjbZCoq+`P^#_1vHd};kT^0Vh!g}oCt`OKf9 zjClqWc3x;`Zz6bMC!mSKsW=*Ct!Q0S;Oh6W3+5N_T!dyq&Y9#M?IpjCznZY($DaZ7 zhWvhLeKV3q<$;#NzwDo>gy+G3)Rd3twzjuD3QCK#%qX^p55Ijp2!kX33=;6Tfa;tu zd>zJdW%S3uwz)u`pMKB-1>yiIR{LjxQEeAXX7`~wbkOMYdR)$hG?DG2!gA;$mtrwk2i_S8Mb;->2F zm6|s@Jg&LYCio(p<2W?IdCDiUbZW5X3LLN2u7{iHlMw+H(b zX&lezS~>7JCvXz zi=lX#i19VLIzM|o!}7MSNThUjh=Ue3f@H$-hK}XTQzKC+y~Vep=ezb4nW4OI(!qG( zURsSAQsbkt80RGyoBg=I7gB+ZP__aV2HVmKaURw=V`D1J=}Ez4UCNn_##?Y~>n_o7 zou#^!H3dI7gz$cpV5FbXq4jW)+u7l86pD9tT+q@!v;+zX=MDP1=dglU{?H=l0Vd#mT-xW=04bB_ak|#6i7Y= zY)51RpCN|C2jr0#LqH86ErQTT8tWqO#Z+@sg6f@2AAWy9lXTTRle;JM}$cN@qE8Pl? zn&A&OR2U|q1*##SdNKH!zoWi#;K;~SpU!@q?XBDHEk21^W3a_!&1XPvDuw=)IlzN( zMj|WxKy!KPv;=gyOvvKTD5k-rXw=kwfc7<4|4v;d`kAG?v;L;F)hACeezCgW5UP~> zik^M;4@F29keim^jzY4|*t3y(^#EXRIj+s|zsU4I!&Gxg6y+D9{8Qw|7+KJQTj`^u zpMTU9LU|>Bm?s%1yZSkJTEVY-n4}m=3hm*wIv|+VF9-Nm5+k7EpeonB$gb@L6kAiL zfB&Uu-**3_a?7$N$-J|P_haD9bZ(=5+i4Bc|JYx3#{Xr1fuk0btdWJbp$^+`{P4Qkxg1Yn~;O5j6mmo7laN+(BnbHDuu$& zh69KBQQ(do8*(}qA;tbF!vA85&3~%H!yj#S!TeAI z^R$z30b)&pm-#U?oC_bS0VlI<9=Ht2fLfve!zn#VFyFbh3qme?9`v44E4h@QP8D5! z$?y*wKIEF4}0U~n^U|{?Bu8mFQg7F8Zz<{5t_ zW0vrSq6C2=)m(ja*&yx*ve7enHX$|s+6gYuQ}7|NcI)piLzbOSE0Dw+o%&&0AqN7E zdGc=>&$%&)enl>Wib;rdep>rpG6e5R*MtEBlNktzTAL!v^(WeLm!!peT@C9HMO}zz zG9Li)OYGG&dUgf5RvIX?YMpL;8}3q;v^ zM$;gNK=gA%^x0zsm!Zq0rv(=`)GsE&D{~`m4?Y%`qNY8ZBAB%Fv8aew64g>s+tYU2?2Ma2&zcrq!gYg zs>{vvbsz1XP-5Y@hV2L*5>c2ZpW=bwek>TG+_(2GTveA}j3!urj~*F6y^NXbif%Z? zmg=fw6k7}nS*0IJG28;nYtWOg5D((Le>{)UcW$4~*ZTH6 z6-Q2A>W;giFF1`r3r>GS?XA9FV*gz(Yq}+V8R7zIecu6Jz)@6pB^fTqg@hTRXV~$h zyFv&@)X!U;i{`d^)GD*++d^WB$Yj2vJ}}CFeaV87jCjWcQpZ_CGf%WEk~wHO38SA_ ze_?W58S(9mPtcMU#~d%tCG)bDKWSs$0f`bSOxqOY79K9bE1SQ>?m8Fue9+Wt7Qhq3 zYtorPqtaNQ#L*P2c3C5u&}{2gyR>`*{iUD?5l@=%5QI&H>O4tfNv`%*al~?>*k}J9 zJ7{2DqVXb*mz{`FUt`ik)(2CIcL}i>9Jw_kI>uTo89oJqv5T=X>Pu~?$n!)0ry86n zSy0&hz5D2{hCly)4${1nW>Psn`eTk5qUyiu{Ju7xMaeti4<#;F+^$XQM;KCVfKfH1 z&6zkuSVNG$MSV`*gySm$$NG_?BPZOiNIWXGAfIgLeRMS(k?Zxz!Ip5lzMYomQH$*I z_cZR`HG@jB=`*oU3feDI3?)HFVm4ia<;~T|#H(FD0#qGK0HF$ce`@(ao&lwJ>urG$ zo#)FTeTV3(RHVUINnuoV$o_mj^Zspli@*Hg8Rq=JUO6stdj8hMX~^Ysx4!+74g_C) zR{Yz_AIGI#qJ{k0rI3zm!X2RQI-4{Ez%5<*?m#S!0-{vDRq-b}PFKqEh9OBrUHAAJ z+-loBfvP#OR1FFJ(+RcWl?OgxtlWNNMoTQ8#+|vKIR%SY7RSR)Z92?mAVhDQ->uX1 zi>+ssuR7ac^MY`FZMJ+`{Ri>b;>;mimHX`)`vmc70#D%zsG zaI$scc&;!t?*2g6;^!F4Y}E!>17B;p-_WBPci#p*_jJg)Ftqs5 zj`Jbgr9>ioQ{#rr&>NjZ$gWFVs_j`hZWN^qo2M;OBQ^kk#J2yHK)7TWN!GBK|MKN_ zvcfmg)KH=~-y6HoRR$F6090#3R0QVV(Pw)?Xamw%4*F_;hM0P^AwE ztauOM7+rP-@Q={WN{1_^^W9Cy?LX7vr`f zqHd3>&(DJN@S=;SQ&LGZMgB7+Fp1>+^7(Qx|GE8;?_JXLt~VeVu>k8tEp`D8@7t*+g`Js3 z5?j5?oVqBY=T%5nZ*8(tx6c@z z@%=fHJjp_XS>jM)1|{XF?DDEoaQ++7|Cq0MqedQvjS~r!_hk;j@w?(9a(Vj2z2BM3 z%}XOH6jsUUwlO$1a`T$1=mxSrwj^={vd#e?RatwO+79S@g+t#QanmMi!2|-ls6^JMNnaDj%!5Ba%}kTJ02j*jZ0R53dAZ_(GJ{0 z_fH)jwH`;|qT74q{gX)A$RCUiA)^Nn?i8yMBKa9z=8X*#WYy+|U7HU?O1hBjVLktr zjwo>J@r&WFzahnZ+ON{0u2XZFt!eh! z@}(=|HHzJKIr4W0O@Wvd@`19?D6z}-Q>^dgtGNNGg|iycpy<~S=1IOn8??}&V4R8D zA+;>JVmX#iGl4!#6^>VDvKBp@2>}Koh|#2+F_LqYTqlxLkXEk%ayHkxqo``mUT z>GbdTbG0T3^QJFw6>21VTu~k9@=;0=UaABrFnQ9WxuC|$blpEU*Bz5KQ7yWsN%)h{ ztr@4}O9C!o{md;aT3YM~+Jp$eK!mRp*HBqW)%f1-B4&_}jDut!F2DQLxQimb;B2ii%s{I&Bn~|-j~CwOHt{10fLkB6syVR;dv>(kNDQb)UlYNnn9=L5+B3>` zv|Kom`nj!YAIu~XOa!X5o6`MHup`uL)Ln|&$p~L=YJZF;l}V-lC1{#aspJ7=p3ki) z>jn@%*0_aTrKRhc!==mPZ>z(!7Db*E%J1~;?H3xxYwIp2ijo}PWg`e->+G>KC<+Ee z={6`ZKsyp>7?*)V#`;4*g?d%(dY^a2AWD?b;;TS_tUrYfmfn|S^V~HCzE}G2r;_xy zsw;;Bx0Anagvdgfq}hdX<6J*S4+%t6`L=5fD2-k;W*F9P@Y!@7ntd;S`{g`51QebW zRQdJ-Y%rQKn7~QHmD(r*ce{MT-Sigu&#jaIKIjN4eSwdL0+g={b$ujSfU@ftGf=u} zBlWD_uBxT(A9o=ju|N~jKmK0Jmv@R@YrA(Rd`D;Iv~Y5IdO|LtcA@)Kt=`1pr8|%P z-nYUze&6TN^-6*qo^++7?C}Mp2bv0}NO$4K$276=#!_oT9vm?kWEHx1idy*1D-UlM z-?6P{XFyeU*CD^HIkr;3%;R)u^_WHu4)e(yjlwwfD?1wP^8EiCT5Yf*u|K%i;({-t zKe}vv^IjS&|ICN@E^z8O#MyRtwKY_IFCZZFUY4N$iA52y0&I~3AuRr~x7^_-yt=yc z>g9qp_04Rop7PUGA3!1ISVoOJOd31kFbk@HuDi z&4acwUrD@fLUcW!CM?r~W3Cc5VLnqte$wI)i+e8>hxRan6PLQ3uPc3N#PH4Hs>vFs ze%Y#hiF2hG>*xqiid2QrbxY_J$m}% zh=wdIHO)4T7PAp5fo8)Q#GISNEt&SNUwdYENI+-8kL$FF*p71*?8e6T2EgJ`MrkRr zdlB@Tr_jHvj!C4f00>sk_8jff%@6XnAGcgQ>HEO`zOCn~xo`wTvFSm6Z(@-Mhmzaf(B=c3h}B9)I& z%a;xd(_Gw^K!0y3uX?vv$;5wDDELNwxUD8Il&AhnN+{cW0!_$y95vI2bOd3ENDvJw z+V5kNX{SB16QcJ~rU{|=4lVL|4in(QQ3+Pd?Lai4lOK`p1L~{qA*(11yz-OhlbklM5VW5xot*ovE(B;lMp4uVJ-3m^ z@^xb4SI0Z5x;g`>Y~4f^q8#@<9NH*Vn!~*HQhYuV#=As*l=CA^EiG{F4f72r=U|o# zaltKJat6$ITtQgy*GmCsw1HP0GpnO2u}+V>SOH6y&rt6jQ* zn<5z;Dzvm?>`zU+naq&AJiG)9ZZ6?AL3}blqAid}5(}X%+Onz|NzaJ zrOnGw!?h-Lsovn;{QScEd%$RldWZ2~qdL#y@uN)Vz~Sbxk56+9&6k>wx!W)U%?i1LtateAw&As2vyBgWxOGnM>NU%?``_vM*sm^6it zP09M49piAN(jK4AVw*B%4l#XhK|gNUnL}qh>2i+dStH}2)}^!gUibh2C>m zCx~H*cA@km*0SZ|j6){f3vI*KgP*rP7RHUi>cq-xGF_#5fo}_#SHIs~s4PG)$%Z<8 z!c7ymicjm8&KV@%?&U#cc#c{=>TB|zwelTVJO#a{CSeGhZ=Vjh%RfaSnKEIa_9Ru4 z2x@{*%4~hA`&;T857+&I;sZzjX9o>rI}j}!hLY~|OlE2ABGPh65Eu{i+?q8d?{Kso zLTGdH+0F0^#qn)Y&W!TT{)<20C%j&kswmQgqq||1%%r~iSi3aOxWhGd@fS7I4fHDL zndlyVln_aoZR_{MT&2fG7RmM1u~+5jX-51?n_|C*2UCe?TaL~RDGdV7)uajJKa`1O zg+>2{CPnw;c|Li%e)U(BnI0>UY>~UTqK0R<3X}CEW~mn;Oi0~M&AqTOXOB_hT!EQg z6WsY>*n1~^7140B9aR{6ng#1ZfKhuBP=<(M;tdMt??jb6Y>t@8W!mg|M{ISe6uu;7 znKG9l1DetMRvIn5e3Otd+3?&3>X4jV70aV_3N8uy4at6vTbQKEnlJq-f>fs9bl;0J z!P$on#N`h^s zD|ZWH+Omw4cS130X<^A)UFG^9N*Xz+R`IyG_Ts!_obZ#5t_k z%&zhtWE1Wv#l;CTd0iNq0i|>$w`yq?3tc$;&0!vXH0+M$TV%Gg~Wj(uYP+y)TmLbk#zOh6l* zaQ7~ZOMd9fx3;;XhHmtJQf?wOY-ZW`=)|vI_gF2*voOh z?&vPmHh?h)6~3W$ac8S$B$KHq&B$)f0%%UzKi7$=kb?6;mO>?oV|w8(@{iM~S`KSh z%AW=YQ=M9EmCsk6y5BQLY2YSZe@%O7t5GVNP0f(wf?HPixEO4<>S=)ui{&$VMjJK$w2>h{5jCdLP+q zouJpA8^xe~^^2;nDHjpx_~mzPukxIV-X}rJEh^7V2eE*}L)blimEAzt7 z!RK{HO);AR>qUPaNkxjZ^PyFM+2()q#B0i@F7>7-FMG3OBZ_!=Y$Iu z5Ns1)xLc*O12Q03XlW5jf}^j7**+RJ`MZ22s;z<&oK=*hE#Hgvz8c2vT`L?YM9G1V z&s86ku<%EP6cxG>PSij0+&Y*Z3avHEsXZ}&XqGqw+{3?_Iff#37ja%tXucF_k|y`o zCX__lv=giR_;rRvt%@^$fzQ6^MBIH(%@XKuU%oP#b5{ttZX6}IysIa&0RUD!QRuE} zm_Cw8KI|oS=14@G@rtjvh)8>;UB!3a{X@&x{qWX1&1tzH^ztIhjWo6J*)rU$8K>?q=d>=LheK)*(Ovmdk(5UC4RA&RG35>K)fk5WKsjGU%qSW)s$!vYjx z+>)-n=EOp?mVVj2HoD_B)~!s0r1tfi=Aj#!Qd>`;L%c2S$7m{ul4WYjEWEYsw^?uG z(=Na2d3U+4%67d7e@UoHFB?x(BVrRL6j?=hVBaX_M`_onGT8mwduMrmh%-|fwb$ba zZ?oGfcnOeTv1&h8u%E5)3`;a*3@?doxN#}AmcA^xkp-Jxm6r~>Z-!RxbAv>x(Ap2_hI|kdqAt(9*i~(~rnoWV0BSzFjH+i*{bc)yYVoU%F@V$!^ zec*}5vfg-(kbBNcifsL}^~_fx_=VCh&1s#sNuGZpb2<`SabK0S-@lv=zu(@8d3&w! zV~b>cpuPLo&DHZh@d%#I*l4aH>7;M%-6sb&ZXOKze~WTNod!0721H3Jva8)~TARfE z4Y2Vl5q=>FvAd=R9R?NEX#4}#Y9Glp!U$XwnIEG3_ZQolwd{Yx)|a2~HqB>NXLO3m z`AdRe#ndTmFNrlptNpnsXszI(ig!u0I)Lypbwhj^tk#&7T<&4$S`~S~GXiN)5x@NJU$qS zXnL?WOg-j*D&sX7FPDJZTQVKhDkXCO5b&gsun$;ve+R9*1@ z^UZ#Sj268=PVS{NX=uu76TkFJhaI|2`nF&CAAYw#rmNP$3P%SteV#Alr=sSgItbcH zIvy{6ho<5a0+D-)xCb!#Yd4lKnj)YHE_eiL{W|dBWq#WLjdvL57%F2f;P1_YJ0>Ib zIcbE<>MfzsI(D3LCEdco3$IlWrLJ4$|+& zD&3>(L!-7&RlbB6Anqej)!j}01v1r_-RUW1(=leNUUibKa-p5GEJDC*E6(95*_!)Gxmst9NdLwws0t*}Tk(rU(c!69(8HZ5lMh9j-kPe({69b9aS+Di%H9@CuSFGj zaY{NTlE(>MP45D~3!v(oZVN8(?vOl;Cpt^N@dJI2*hG!tja&A-s_%%nkIoZSXD;tR z^4|pFCAkIX2e`7xv5o}%TPOXFyVsO+=J7nc-J5!YPS4QqG>7eHju)Qes}6c`l2hh* z26$1pU-2Yja|mykhdaN+`rbiu;E|#HG@*OYmcI@L)O?%{J1T3!P>cSN>a%s5Gbt@y zh}nDf*0xSa2TXdH;l`-6(PWOmdtZ>iXcOYA;{&=Va?4CtsHmC7eyAvV@NcGL%U9uL z7|vQcNvN8ZSF?KEkZ4BOgh%tu9^!~ln|%Inx40RA>0>lu+Thpgn8CQdkjaxX@X=YK z@h4~Q-C1;it5-V9Q_3#q72WUk8FwDT>kdKtW({$Gu@`u$194p6CwzRGPKU4xqCS&G z=PRMoWuofqA^79W>%*4i65;dQ-%g$9KPT30gw6pLLMlQlX-Zxa5xA+#c1e zLf!KCprxd4uy)#FfNHrU{dDvQq0pm$!jUS49n>x10W{{G$~*|2D=fE7QMS@7GgMl6 zSXgv~8usY%MvX=uTP|DGC5LVMt-E(ooW1mpD`dkj%16KjNgM8tUVdZ-o-#?f?*uW^ zNJB$5qfTmA2)RPZ1y-K;Rj)cdfJVZ<4x&zb5}T6}@YdvKSgcl2U8(A)%haOo8RsaK ztg0o;(a`Abvh{9q(Oydi;6%|qR!q&e9zc5FF#D$=U76ae5?I0%$8#if-kbHOcEEux zPV$R%ENx_BH`CFXOeW{IuLTS!eek;a^Z5N%dG|7p(+j6eXz{9_-}mD>nt|Sr3>jk6 zq-3OG>X!#Za~!k8Ut=qnDO~T7xoGg^iusoRRT%l$`;j*B7jAweufTlJ(;h}f8#N>h zrXN}qywWfJ?uyg0LVMMy`GskUR+w|mECovBwY5jkfc+&)MfwQgd~QRRWMcQ^l)OqF zCTu}3SC6alfFr*PpUp>GoK5quTDv|LyY{D;TI7m9 zV56R*>oS;4x36B)&6!7#225#L!c-e(&6hTV-l!f{V}M?Njt;5MdXgu}ds-T?b<9~AF!xpCpf(j)Sn2^uq&5K73&cX|l5qEBmLEe&7OqIfmrn?E5EB^N5?g^k& zgmZuZ3Z8X}L|j{EghY&CyMAU7XlrQva5Ah+uKculM<{#X0|{k#YbNpbpK+0ujggIL z@2+m!x#oQUe8}bTz{O3L2fUHkuF=hOuuI1YzFD6^`tnj}N4E0)EGyY5nP$;n)aAjw8JT1$IMcxMgAUPe7lveoo z*pvX(zSlzSkuA4oLB`G_F8li!h7)xch(k+YwpSIs8x{^w)F-#?7;a^LhmVTiwyLlLIi^Y@JeP3K)t z{2a@7>&ZouF-SeL>X<_+A#DM)t-(w&Pe?SdlR zB`u*y*TO0t(%mIp0@BaL-+!K&=b3SwamL#C+Z`J+$j440)g5(RVu;)FHGt;uo`zykJ)C=EXnddvgadK4!+Tz zJ!iL4@(=nH&Gl@f^|k@#zK)UEI`&0!vXpVpdE(^~!nAhI{{rbS>blKW^2Jqt_#NHW z%eD4|EB^dvj@5*D$L{!>GH&X-+Qp^22G_q6*(_s96SDI2ciTCV@CA!$9V)e{{x693 zZ~6S6TUmE@;UWmd&&5;9*`56C67wpkY!x6Y@GUE;GbAVJ@k-?ThDskn3~w z+EU@w(&`A#*W8crmE#fE)yNuSD?x={a)c!nD|`q)2@QR*e8l&5Y2ZVaAR}WCiW+-y z7UybuKCyNzJ(MS^S@1`&C$-A5Lq%ug(s`XGQl0)I>EB%<2A_W4S;ih{Y~)9m`^N67 zf!F6J6Wnn(zr#);KiO$A4MURX7t`eN^HO|5FWPGB{?6}0(l5x& zykgy_b{xVpV;SwxTbjM;)SQ{p~36%9&sO{dSy#054aw>2*}9PFA;PHF~Ki)hd*A~ zFS;Ycgjdn^O@hVdUv`nC32PP%$;UUn2Cc8$?r0=j7A~3<2;LqFvnMMLxBldAw?r)Cw^EYi<8wDom|R2X z(vjGNvu9VkI+b(FnQ0l9m6bEO^%)JbKls^=xa08R-VJPwX=n8Rg{1keXtKo$vbUC( z;PB~oE@Qjm%%de?bv-iZWB(efIPKo@A0Noy#%(9gPAx8t?ovoS)_Wx`%@il_n18&G zOfR_Up@FWqS2*rTA0~bbr$;Luf1F5Aaa>9UJ(9q-@gNHsB2AK4bbGP-mr;T-!#I*% zMq~i>;o0@U70pKQ6*AeN!U1-4bHgohSZko#U%|L{VeoRUk4E=<+sChMhv$cq{_`*& zy0hm)9>PsK1&>fS};{DFMn7d_`vPVnk`;# zjvP84JFK|%1{;JkowK)tll{gB5mDgDW=1Z>5x0sA7M2FOyv%s$tY1_p&inoss@Gjt zdJG#4R8XkhvmUl}cBZ;e`Fb4vqVO9w>C!QK<9_)z_H4I;KX}If33@+QM+YEi6b-Q~ z1L!Jhi9m~0hYMPYx*YrTI-<*{^RDTRy`Jzu$7S(eq5SaX(@zzwZz*r=LXGjLiQ6J- z+&VE)`Mr1==yUBMX#vUjsJ3pajqTU)6*rQf7K3yop(@Jg1-`iV&<8S7O<0D9qHe8*4HT_>&*aX8Yl_bCk~&Vey*es;?>_(58n^a|>$fw?|KFu08o5 z*&?0=@J3x!sG;85lkx0LbWthyvJz99^WkGZFU%Y1jhFm(V>)Or=qYje%VD(RCm!@L zCVfHQvtDNOf>5_PWs8vDg8He|h(LA_nnu=`hc5?jV_ry(X6VnPj$!WiO(tbsTmNA7 zyX^Nr_5Oc;5Kkf|oyuPYZMXei`Jz#gQe2idQQVnFb4_h~-5Rm2pK2aEH!rQciug0Z7IWgp+hAT1_i4HOBJQQF2$oKpnx^sqUE&YxI=8QwF9{RO6 zwCD{wb)3s5{JVn>KB_T{5Hfp~j&!qYnXq#n#55Admr7EcKQv@3;{yXs^>qetFTKZp z${|kJHCQ9@EvVTzN^^-dLtK>Vx&px0N^U~D^9hUhL0sM!M*y{3jS=87pkG>!zgcm zTZ2@bfxLUmoo5Aq6kFL)k9e$K-J&J%dQ2$YSKrn8S@}4x;m$ zzF@?{20m|XR(+0km}?koK8=dp{@ToTg)u8URs}QipDdVYLtUcAlTbNk^)8oH|D)~n#_H^ow!F?;l!;G2jWZ#k4YD3JXWj2GPQNhn z#hA-PpLeYm`))d*yIHfjQ3V=KQ(1vqFVuF>PUUx^pXv&5ZbnqwBO-a7nR30o{bOW) z%$e~;6>Oc7{N&kb@fP*=Ih}^vRJE05#==r#-H~U}p)ISpg0);#hY74IXFlCfLIn;ya z)-N}Ztxw?4QP<@9z17*4>iH0*R&@d$Wt8kSUdi4V8RnyJ_z=g2r+o4MgD)Dc>t>Q7 zO`&T^=oW|%%1`UIp(oj%mCYwZTD1%+6DjD~!PpC*8FgUU^k z7rZkr|JM}pZ?(9LyF)mvgWz=JvTG`?XOI{k_ zxcCS~g3Gb;uwg4SiJnA(FP44PJq6S2-e582tQ)oLUPIe_V(Xl%ztgN-Wz+@v%-o>P ziKX`t1kuQKCgz;MqTkjdcV9kt0H*^D=oL(fo(8li6%bV#o_jvadPe)})Kj-Rm(`Dfp6sgZ>c+6dwtBPCdr#4iT zBj)h%_M(gE^X<9pz1&T5aY_zrlc&n>yXBX4UMQ8EUbsB( z1V6;HL4 zf-R-;?7q?f9;XAnTpT)6ZafRZNt7glogu@7D25yBxs5s5(yViB@ePn00F;HGBuN7y>?HeXv)oHC5u=^}RkPFScSf)n;!7BRG4<4^h@I~PJtb~l|icx`J&`4!v>RP z$I|m)1>c?zWPA-b%MGJs342u!52%8*KJ(T^ttN#c=pc`=DbmsEPL&v z5na0ai_Rx&?+;3P#XAqhD&~rI_v@miC`HvdiHaL~)aL~fZ1IadUL60%Qp(06kM{f- z*sxY)#(AX0naS~3AuZESQ&>5_cboi(`I@u>4M**01T~(uV!DoH9kfA|7@t`gqAQ+k zVcB)#BRb9Sq4()>s85k}PKgy6Q4B!o_Rauu$frsLkxjvGFB*|$w)sX2LE4YkO88ED z`!8ZhKxhA*z;QjNP0bUe!`qns>fSV&>PXgJR1C}tUGu7c=%D=3C*oapF+V7ITAQj- zD48O|o$KxH>2Y$>dyxBJWz{hzf_-&ycPj1n4{e3Em)L~EXqp-k(d|}ee_^;R|80e z(hpwvgVoj2$uDQc$@}Pw7&qfpY|$tqlAl^*56x!J_dLAk-Z#QshTG((WQJ)yTVW_8 zY{O3bae1ohEb(&5S+{#NG_1jEbQ{6^R{9WjUMpoKUD@7xoj=bI|3cA1#A`IaSVm-t z{A~CLb-ZyNJGvUvxv!CHswEV>4L2lqw>vI%<{jWkN%Jg`P8kVe<`W6SvLx%gj{WaJ zN<^{F`?y{7&X>$X+uxHs(l5s~%uZ^@-&M~c6)O&dQrR&dH z3Z9tmO(2(ytJZ~ri^bvW?NI8_Hr1I!acVa9Sl*7$KW4TNa;7X)*sQ!wdmVFa4^tkI zohAqjTBH(W>z0X`M&9COQ`b>9~6IiY>45qeY)EBg?ho8 z9jCMYLGNeJ=CXNKq!y?R(88D7r5eU^Uh|bt{Z}I>LZX3>s*?nK)d~hUoaCv(yj__U zhTfLSchD`Iq!Iw#777J21?k`DPVBb5MAE;rOQl0LiGjdCndjL%UAb}g_K(GvA-gXu zU=5+(MXMXUD$_aMgcpsdUC7FjnbCRpcAJLWbD9z0>y{(y%(z+n{l%+t#pEOi9h&Y} z-a6)fZ^3fu;#o-Q@wfE7wW$du?5eLlYkWf`1OyPWwY+eK>3sK|ySTwOFYZ-D4w`rpD54uhOa(0=mU1p&Km|z88y!u-j5#;>~(k zj}VaczhGr5GE-PTg&lI1I??@-)OuFxuUztm@0?}7KQGsz-$nY-oa~`hY=&1&gyz@a zp)mK2w>xPIzD0;(g1rD}tnaIDW1XY`E7(B?-2ybGm|BYux_c*?Lcj8d0YH$^HmeS~1n2SD#XN0F%eUPHj!Z~XeEfAOzjh>JqQyS-(ubfXhJ`=})hV+R=9 zq+?z@dp@c6Ym=76Ogspbw@rZB%-xgR*pte6aq%yMlFu*tY~tSs9Dao@M#_V=4b*zU zV?r~C0$CHo@`;xwIdj7h<$sOH(Vy{>sKt}*e zM=+Vz?WGG^BnOA-D18WJ#?ZNk`y5JcwxHIA?8s#RY`o*NL}yz>o^38)+SSx!BZ-}q z$S+Wq#LMpen2^u;6gxuC92{9H>B0|K6MK!q>s6H^e%FA_umhdQFkDeY`tP8+Qp$gc z5$?cV{49yUe!v?=HE)dtHbHvR&E@H8=Jn`F^JiM5oz!n<7<&`ld<>@x?AgC4w>|3TPNM-0q=81IYyy%dwcll4UNV{QpD20!X4V&d(~JrkZeq;*;9S?w&{kqU>z?bp_@a z$fBU#u8rC!4KgFoe5whnE+|e#L*B8yid@5w2=-7!1AM!}#Qc-$joR z*@1OH9!H+1R&cS%a_~@i_cC~mtfTA&Y3@Fov*5WhQzzPV?NyprVgn)^*Ec%bi%D7E z=V9z&IbDXYxQfN$`BPsSrpaU{ibP@MKWP0J!qwDEy&wmP^2D&0+L-nR4P(9 z0nhDyXB*Dd`pPw`^;|IkhHXfg4qrj=6DEgh3wiuULd~KSA88tg9|4*50N6g`&34xQ z_0EG@y|WpDMgQx;c=IapJ(22b2Uuaigd8bAEdYIW^q%q;V+`f9e!2hhmbx*W1J5Kp z>00NC==ZuFy7&(s#IuQzlv!3Lv7A?3q*e~?OKHr=JL62wO%`10NOADXczi&Z=RTAy z&LEQq1+;&f;_$Zg-@GVS4R=hRmP(48`_C?$$1&w}-3z6mFcke9rZNovGY~1%)F;aG}7C#+4HI7H3~my4zM1G z&X!HsVT&5)VR=Pv11mV1EqwNjD*R;llqI7ulS2yL4` z9Y1Ef(a{Pa!A(Kr*3Eo)79cW}bw5^-70fSZ&lZ{w^*#0)gb|U8LUv&5F2lC3o;guR zFTtPtj%JD?mje_aSKLH^6G*p8STaUT2VqTu1O<(=I$cfma|iX7Evhf7^{ZR#oVc;n zOG~|sgsc(|0MOnjcG*vgK5n3rwAtNsqIv6xn?zcv$JD+k7GVw-2!uy~ikirX_ zk3^;CpaghxC&1-+-vFonF5l6;_}pUvGKCFrKVV#oj!H$- zxg43lZucu*K42>NIy<$%VU(77t!TyQ6gaE0s|LeI%G92U*@c}iWT~LB=jW&Fo2KKd zRjA$^s^wKZCQ*neHeu)8Eeslo#uBToX{|b=l(d5#bQPbI-a?Q*jxh~G+F|!;jKu`3 zt*~DiPo?07cEg5CPba9${~nW`KX9W32?o&{$23JbxiF|$^J>e?^$q(TVSyH3vdWKx zKYUGFF(bT?x{Zdr>1|}X4M-~Q+#0F*3P09xFZH!FPzpDMM7C+Ie|CTfe9SKtvVT+6jQO#ZiMx;mzu8^;n zyRH}9OCE^Bc5oD%TQM}ZP|2kw^kR*A3FLhHQMv{63A4!L_SeK#EUN^yEIb`hN;5K# z%Wn2|x$Nz}1e~H|5_R4OQKet>8NVH%gO(Z}d?*=`~Iy9j-A z{>*vLL2#SMb54QBk(YcLl2j~?qDQk}Z~ z&njVg1GN>G&uzeIS0?vk#Qjh*GXb#{;=G8LUO*m^zPx_kf5upMqOTtYNpFnKKKu)~ zlIRqYmktZ^oT22-3M|UE`}AE}J=Ls&t>=FZmRc|LMmLq2M~vK0#(Dw8T_=j9uEQh= z1r`b=?8?)9NYPERNvHW_9(5Z20XQjU%VB0#(Ri}Z*;@5*l7^I-D~_7toYcncT%&Sg zpyxgQKP|6I;?65=yOtvmc!xe2GQ|-9nig|4GAqj~)Hp0>>F;ccBeezji2x1^+-|Cv zV&Xhq1sqjMR*ae5j4UzHt5Qb4-HxX)G%vcsbx}cB$xzUmZN2vH=Wa@TmvMr5radD( zY-&e7La?#JcZG1@MX+0r?&kd%`UwasUgnMaf;?M)xLdu4&i4q;4EGw_RQ+yk&@1K+ z^rNrV$xImpA&sX5Z!3i2AKR%v6*ow^>bP`3du!b&7L3n+o&0-$ADvZt4abs^DuL}6 zMA_;UuUJHH&sXEuIW}5ZMywC?GbxTbGL}f_M#8A0U2XVi`XWkRsMSgN+4UXgL!ZR* z29oy6Gk-FJiZ#;EnaI7K`x=Mv%Cd}rAfcb<>7DoXztem;O;cm6^MFN>nVRK%i{G`f>!Xt}Bd%{0u^J(+fueuR{L>Pq&+t7>{!QiOD5U3&fzRysoNPoK@PRW- zo==F2$l}-kV&-1X7q+A0e@(5ld)v;B3CedP0Q53rk6boTp;jCv zn|5M0`5jv_@lglg_@6b#ls7n5C7#M*EWRPcJ8LRVO&XrGD!9_`DxUsDokRh#DD7+O zOX<;D6S@DqdK`aCfi^z&ho*QykaRvgPXKA%IGe^LSbw@rxoHkk?i8xaOI$ZnzW zt78$q<>FECQWlob(jD3p6yciubh7@8!3%qCf8H?gu$d~1{h?AyBB#3v-J?8qQo+UY zs_RW)Q#LW~IJt$xSAZc!`{At3gCdTJ#)z!4xd*a>xEO5R^#|8zz&-%}tUi)`vsps?g1fc( z;s~>j3t|C5ig?ji%!&OSD>N9)eDZtw&Bu4&XhzMyX5XK@KS*Z#U6j{+_&bCU&hYg@ z?h7T+lNBX!MM#|T0auqAm#{RJC1ckbYnRk(NgbOduJHvGw%zl;22+GHNTlw6U9ar6 z{Yy{#Okbz|z@xaQr!~E`a07)PtdH0vza z8WRrW-u6+yP~JuQRE`jOPHSD0QPE}?bwfqYPZ+KEwEw)*4BMDklDFBK8>}b&djik6 zfZu&XfD3q4#>X-%lhQ4lNlfG5yPkQ<0B!Jz1i4q@N_faVM0Tk$g;{=L2 zPwJX)m8~L;inv?iuEx1dAK;%zv{m=vaZbiA6Azx5m;K&XPg@BZJ!^lulJth=*K=xd z9cXrl!BkjfLNrnpGIcW5o-sfmZ!DTN75<1F&@HTF_8Mhsq^UdZx({!*)exj|?Wrn@ z43Fbf6`?_RI_L=rUq^e$V)_n533C6OvDRo1eWl|Ol?K_EJ)8A)9_hNTv0P_q{pGNc zBvvtAlT{|{>whcm27Ig|4)uT82Y4JOVe&73@k(_om2wuf$>f#IJpJbDqg-%B@rf1b zc_W^KwMzaESIGe%=s$2cLyq8E5Zp!nJKvcu2QG4_cTVNImeud3!PiS2TAvsT+geQEP?90(YxTE}emy=Zk>L|3~& ztx2H5RU%f{!iw&V#{1aj!t}6bss-G}1Dsl*0jBFCjj*#n)@dXSR?KqR>~+}L;36_P z_UhYIOV8&~mOP&n`3fc@KGg}wn+f3bGg+v7i#KIy0vQdiW(9(DUcw zit=4UO`m*^sj>M0v(A8VDAzfs3d;`i(IgKp@Y;Rf2z(C5_-Z&zXH?8_ovh*0{%Lig ziB_!A?}6mM@-S(vwEw7|IIy@O(bSIhrPJ2cqp82LGYF)7DNoB3XKdnB)F701oS~1* z%gNz4-^rHoFqYD-G_~c3n?KkZT<~g@jKaJ zz4+WPvc2KdhHJrdMf2GYs|-!fRLC81Y8l?buI8(5cUzUi9#G?f@zh`B=2Ft#q$xo| zteV{EnP}$K3Zq{|3_Vb);i&qR?^)u32NRNqHL2>!sc37E2iGq|nJjp0R{y*!(Q|+g zJ3eXd$(}ruw$GvLF&T1IMTq7fx=w@S{c52cNEm_~xe~bQMe43!csr7&4-Fgn#IRlp zUe#l%`lbHVgD1(<(cZT5nZ2osk_y1LYAXf<&$MT0kH@;9BxWJCJ6NQx>(yJ}ogfN4 zFP{0*7GgpGjv#;r4(3b5#k{AK9&Akz704o03E;m{H80pz--6wRD46R6_uGP96p?c8 z3D$_=&elqffBo)=BPg0LtemTPLyn=SuRmoBDjDcDIV^CSmA{uq6QPd;5bO zh76i~!V3xFb*5)Km{ky;d;>P*rtiYW(&+8?sNIi`DTTNNvK?x&PvA0aXpsME3}&*L zS^>a0=lhY`1rx3wnmeK`Gb8`Xt!9Ai%y7YDuaA(AM4@dUapsF6vtz0CN9MSQ`x;$! zl9ty?ma#9qrxe3-R4_Kgs(xZkmS(4vsY7;`N%B=g5<(3zSm(z=ApA}u(q=v6iC9r- z5@b~xn&o%ZlR$6)jr0NkcN+T>gw9S|)t&b<^ElfTPyMBQt##_yAUe>{O54q{+-?|_Q>&w#s8ecvdCI!<98 z=Ks8vn_Z+M9YVs{w)}yKZ=inLTKGQ!^CPnNUPu|Wkv{(X$#5Azk04%*PO$sW_})Bh z*vtFqEt|^T4rrv6dE(IIK50`;5X@QpjSv;fpLtPRZg9pdi6-sWgKI5^3QEPTPcyPg z)HXKTf^O3|z^k}ypX65cqy;uJoh#m)0P#ld6A&MMRoz^6T1AjbOMZ8WM#p-TifQK$ zUs?|^CoKwFINE>H|)f6VJ07)z2ggkAGdtHyfqMFLp5_V(ng z8V>Piw7C!yuHQ@E_H({- zC+9--XeGMviPb|vkbQ1{0WdTAid-wuy$|YL#g^z3IG(kHw?|a~tpp;2y>M`bke3k% zw44Q{;n^n5n`+yVJnl(n^rb(Rkh<|P_AclPzE^M`W3+h>vE%#enA7Q6DyRe1Y+C!& zqRqKUQe%1X_Y+{_GS0K2oD0U}z2}<$t_a8M^A6Ob;LQ#sKLLMXTs2VL;9*uk^Ki+_o;^spzo1mCBQ=|%lJ=Kg5TL72z83hC}SGyIL&;3IU*(~+Dd z@T_gpK^->dzFMNzP8ttVij9bVD#_i-;u>`PeB!1%LyVAgC!wa#-jgG`8G|&`9~aW; zb?sRdScifl_^pUFBD{&Cc+5EzkW@;nD-ZXGQ14@2gF}14In%61-M-7j(C?mNQw3L} zs{5Te-U(w1-=-A|re4XcEypi!!Ce&$X>g|~k?_ud4zxHz(^bb%)>gfgj{h>h?F~xLNx(HL?tQmxxiAVYe_t zx?`2T@U;^}A&H&?BZ{~D0jlWi=wZXo5r83qL2VDb*q9BhnWxV54}@?_BN`%;;WBH`WS3Z+Pg|8BK4i=F3u z4EZ8wcD_^BWJwV)zVLvLnhoTL=Su-trlpPJ+ThjR&j?^6g|b82*7Lu}V(cj%9cj8T znW)Yu1=b!3O<2wn{N*xk81QAgoez@J*+d_JR*uz#j!vD65_%wN&Md zr+pwFRdf=BghZYyx}BY18a+r0uWlPLB_&>qsHyt7u{Og_@c}PEG?D_`p`dG(XX}G2 zg1Jk*`}4EN8shX*F8%nzr875lkUibI6ScKA7Xx^Eipdzze`dst|2{L3KXnJ6;tzHu1!g5VIo6M8OtKMimSkZ`8ACs- zXNsbW#SO6ylSI>X-rZd`WlH*Um?QPYUCCP>J{{Srv)x(`*V_uCM~ZcF3gyl30}_^q z-9$+6^g~}BG4Zh~Xx#R{@t#)ikWfL@N zMAsP#21HvD~1&@u~tbe6ZZbaQfS<9(|9faeskw#M28{2lI_*j1v%f|;h|4UIxuu|(2kcw@Ov=_yijU!N6 zx=3(P{TE#R_Y*%-T3+dj^P!J|Z}J->DH4LuJ_ft6mV>zad8)Uoj5prbV?;w9PIOcs zz|@dD2r3`YHo)+QYP~wVsgPJIPh+KrY^W|aMnm{Cf|{^!*P`Nf{}`Gk@{3wh8Qp9) zg316lz>Ib?AsPaxe;(K2vkW9YPJVW8O{iuMt2d8hD3nblg+vg&C!pf`1_ern5Tq({ zqnpjw6|F=SzAT6o;@V>w4%GJElv8Wn#gTl&SpCL0kr(4lY1SB#XOOI-q>dMiWd%jsRmJ~1tpTCsSubh(O!8~l0Eb`)OKG2F&8~xQ zGcdPlSO8}n@51WmFs)irN6rp2kvSHRE^dL6J&!f+mF=zl+beC=b38DQ5vRmE_7l|@ z#Ow#7tElF0bm@j9%^j2^#q`m$QHOz)YZr7Wv-htqs|qPsRBG?HlK%f3VM6?ltw-Y% zbWD`pd`7X9*{&X4H zTcX20_M_L|P`vDGClGG#2!w}GJTw_bonTX2<|SH=mc@fCKAZKR+JGQU>C#U02@2QY z`~?V$sOk%4!r3?Pq!Y(|Q-zH?z5?AqM1f#2%$56~ZeQdbn!m*7F=mHuH#`~8}smmoy z`~D%<2W<;CqxQmp)KH~S^4NYiVJ9Wh&Y!S{z?95qc=e@xciCjI>54^Fis;7E?UpiY z=Xxh^`le>4sJ{PVhdXU<64T7i$RX`$jUpyZ95i`OT89GXVKKI{W-g@@g zi+Uxr9n_6Ep%?VM*z6JXE$Z@{(}4Tsdg?EXm4y5O2$t-JO$2&^(x}Lw=kk3gmfG2l zYqheXoGsh;ZUfSSUf1QSXnCdc07kjp(v>@WipI$DEv*N0@#^#w0o|-V$SydxoH0$l z`5GCvi(KNG%cjdP-Yh?gx)A>2a4oQN-kdS<->*B@8iWR?_k^I3H`)=t_3xzZut;ri z-`dW+`1QrvFj-bRJ#{d^X;v3QZy z=Hb6fP42E+cxSelt@LVv40VdHmPRq-8A+p0`&dK79dW<2`cLhL_-<3Gv+!=7_{gqB zz5~O>tHfbOs8^a`t)83KFjQWrX>GrE%aT~X`0*Okhjzx*%x&*^`jJ)K-IaFrR!x|+ zTh0n+2&|_xonCE&4rO< zgN&GX(&c}_J>eI;!ROq3!I2{yJ&pXYejVS=v_`)DDeR$&KvAEc9uULwd(Zagy<64M zaY-vE1phg^cSA%aZVbGACbOI76?vAr_!n$b*Rw>gDoVrnI5R3^cZH1!KE;k=3`EWk zKsJQFo*SJ>xxzY|yMQ-epu{WmvfR^w0SEwc*9F01hs#&-m6f0J&4n9>r!J3+aXFN8 zeQEE_G|N2xa&OWxN(6RYcJHu=tVdNBYp=cY2SV6C=v*SrAID=aspnvfjZt! z`NRUc7q<*y$23r{Z8RR!H^JSijfc)ehLniUN-x)FiXO2f=wqszY`NkQB#4%gK^o(V zq!U&%SjzrQ8{5Kt&9Vd$;NvU0yW*hwpB$izPN-zWXj9>}wwJxdRXWwk&El;4ZZ0_@ zf!Sh6d;e%~!Q}efk2mTwF_LP9E}xDe<9U)%VvTC@uSn9r28`l!08_2T_Cr#|bp@eq zIOb98IEcJ6Ib|jA{NN=WE3@&uh^vDp9&bmS$Hx7F(jTn@9#z9gw5@hy!Tjw`IcqQM^x4J@d9iP7O;$AJl!~C zFIXcPIJjWv^R%;150clNB+`%yS=q_P!mPjl)0Ufr1-xG4e=iR-K>d%jNDHH}jD29o z<<@ahz8s?cO|2gjS=jRZ;CavY9{5lESWR&BxcG{B%^_wV#>YtBGG)92 z(C&C+e}5ir#s82{sjq?GCkGH=QXAn|wA$DD29L7dvN*hp?COZ-DKylQ3tLA&8LKWM zzQD2;X|*1HopH#e%x_inLL#z~>7Iq7xNY;9^S1<6*?5*Jl62CHsEbn7Qne+7u-} zL(-OaszjLiJ!bLu!N_$e^!I}v;|1ktdOF@^DikW@tj< zQ~35kGXF(K_vpgPy*n%jJ#vAiv-RvQ>kP(rFOfPfOTDL-_$#`st_9eZ>jJ|j zi<#)G3draumzE`5~+@31n>$xx55z4wc(nUES(E-`S@ZrtBod@oNlx6miX| zVz|n~J1JcXv+xy~lR7~UAcv{Bj7}}XDC17`JgiGR3C($;Y`5BgQNok9`V&Y0SUoeC zCLi;viSWHQgFb6zXc8yQ`<)n~$;#5S!r`wQ0;otK^^P+)7oEtbLE`6ezooR z=12Sm6@~^0pE&h84ZI;aqa*1kR^|r%p}*Z*V5G(;tbW2Q7K&6S9|q=cY9&WcYv)o2 z#U}uDayirAiJgM29_DiU+uhqYA!MoFgjp#af^o>T66VHs09;Z7HO2!ZI&BV3oVeBw z&2H{Q6p1D|ry5hmzfKI`5d81q0bL{a2z7J=8gO-Bb7;IIV5F;B@E?K&q=sc{^h~MD z2^2*j(CYg2SB50KZrc~^WK_{Zho;7UGPcxTEX$8>zF%VBBgj)_tWugn1C!FsV3x$i z{!M!^CImw0wGBi>*X@=BP1wp~K5;Ip1tBYujS;qnJ)^tW+Obovs$~Acb2%~)yBX33 zQBAGwPs0ol1GN@ZlQx!L03F|OYuk=gcDZ8cAjINE$895y2)wkBZ}xiPBq=O z_0LBb;2rD&k^r80V50N_7pnZt6eqZi6Rmto|8y=G{V?Ae?JVOn zfO)*SOVf!GQhl}6ndjJgcJvpl)}3>04ht;;Fv}X(A3{5rl`*Uy(&de+^39LVt10dU zHL6z5DRtv5vTpwUZMzxesw4VBrCG+WfOmOhmzNf%$LgOmrFTxLWK%KH)9a+?PCf6`=gptxhMP%^}6h#mI5s zSG}aki{C*A-bg+QbF!ZqfwPQe>5G;hJ#GwSSnrTgKB`EbdklR$5_i=G;e|QN!=plh@->U5j zd~!+rPnOIq9UpuKbJ|TdCCf1+XemDoRM1M)^MhwR5olOriz_)pLMm5a09%s8tPkat7(%;kFd1!kmXc?d^=fX#o{}~Is-F2eL z%+h_^$Keqyh%lSi@H~F%N1)Lot(w=yxIX;&laR2a8?BPPE^jy{SwnQm)vJLl$_dt2 zI|jUZXtDFHhMi7=r?8s`uu^K{GaeGn7bFTxRyNN55)Hpi8^oz(J1o=X<^pej_boWO z4A$p7k&0(u`FwWd;5`?|;U}Z&92m7L)pO7=alJ$ZJxdj2JIL@|9KVOIhsB>9-;u(X zs>fg3gA_Q(&;<8pwx)rAZjvcK=e$F&+Q+snL&yIE7%-@xECe=$vXTo$)SF6t(#dyx zxJ6jr>N|D5t_>Ip4M1#xaH!CrVeN70L)>~_D|IN{>NMiFXv&zqsJFOzoeyFK| zV~BBMJ*fNf3_O|z%gMoaX*d99MS+Sof<2?Zq7vjGpjP|z>1Xi!8?Q#Z?xs9^)O*$2 z->(#SOIA&l5u(?}uyZ?S_w{O;G9rfE>pwmKU*{{;qIl17ir&_dGTbbnQxy6xzbVDk zkyUkZ-~UMZxLZ2aJNP~jOpUYBZ%*G^5_d$_%`tGetk1yyqE3XlsW&XhnOQ6V`Lam> zTEJY zBml=>*%_>CuuVEwP}9AmV(w{;PWZR~@z(18EwRy= z<+QL%yPIw@CO4|?3^jAJP1>b&rNF^U!oKR?(riGazb`p^IrThde_s8Nw>{l=chMRd3y_@P1q zReuwrXO_@*YL7h1C+SO#O|Ou$Hjx{+$^aoZOHv4+>A_}L-r_5wS01d%dOft3)!Lf6 zS({q$@CBW)u?ANvwUOcTIf*kvV5bKBom_ac>wgqzjf3c?Sm99)wheVb?_1!|GTYiA zNHn?u@rgqKP6}z)GITbYxBG1NKgvfef0k6b0O1F7&0N9-+5+-@aL~GIwEoghQJeWN zA_|>}X1}JcFU`>L-ab`c<0abw*fnQ;k#q5GAysM7!H7tn)LeXB@8M&~AQ@chUZ~mq zIJ1WqU)f_VkCR)K?{9Ny-C)XsrsK`dG}9$h>Lc_7wl7bb8A0y$j^F`3#Ff-J;KRgB z=v#Ysj{;_2klS+awmq3gO^KSVo9Ty~N)8q$V%hG?3WvB~4;-rcxNb=rl)Gz^Mqy4H z_Z-&L_M~WELww^S0B_UvFgCJFfq=}gCH{#CZlvNT8Jrc>XqE6GDIW{U!5&9p${&rn zE?Cxo`HqFG&@PK{`0rtU?7VVvb+!jNQV=d;x72Utxk1kbf*OXMwh!43mmJw$SOO=G z&tcuwU=71-`5n1Gg^I1gc3Dd2FAc|poY9nFo(BN`WO5;;=|b_XVk-&g>SZ$kX6}FB z;}>GVWK2TD%L*j30F_y0??w5t1TzaO3pGNHte2euC@`;$QvM&J&N?cp_l@>LNH-`c z2uLZNLpLfVEnQO5oyvenhagBP-Hmj2r*wCBcik7izkBa;ty#-|z&W1to+tMH>;TL6 z$yJg`Eyf8qx_3+SfKpa{=2eTe0{QJQ;PY?6w|G6)8%Jp8lkMF?)9{M6p;${|zHapT zOAu=qvUrFuO4?>_OnN9Q;UCIpW(Z)A56tTAhpWSYRC55jYt+x7sLvuFrqUmbYE&F5 z1Ccbxo@vfx5}oB)N+m-Tsq&b?h#ru?zT>UTLugVZqQpAHz74GrvRuE(Vh>Ry}!FA0tAHISiKza*kJmR=#Eu4{pu3NH`_X5!-REIT<>x)R9&Y{HJqGgpA5n-R7bZ0SpkhRc-@;)=cQ!H z(AC+LO#?uzx)(^DY%LAYQE(=ai)~4vUQ_5Ms@9*?gvujE8%y;}@<7O43$h*+A|a7> zLO&!3sP?hj>4OSu4H#o_NWw>`&O;ANJkDA4Bol-1~OPmXT12FX1NI)322Y_=7K zS2sjr;8IA|;`)EC`u{fD=i?JP9jCjI)N0r@Jy28@KGVj#lAF6}{96Hh%U@Yw_!pg>FktoBA=)*lfl(#}nMrSVEJ zi{ipR;b6hF|9by@5;FfDSo}irXQ}3A?Km8+_0I&A)eIWo!2hItf0AWGkP=v<7T2yJ z3W-pR0aV!I!NG}B+OW16ajewA?K+%!ypau8%<`W}hpFg4V`UjT?FCX~k_utA~ zn84r@SRU78ZL#T`7*cB%OX*;VGN0-!u^teKpJSSriKy` zk}MoEM*Wg;{?4Y4!Ok?zrRzR zlr-})pbKYZ9%wv`3Gyn(MtcR{N{sGdzb|SD4x2%-hO)qM@Fb$biQN6pV|8E0^nxx_ zAFe0aZ(?k~<;*m$HW^7TG%cR{v-y`ZJB+lG6OedZxvntnAnMM0-cq+a!RU#2e)=z@9+`z>KxybN*18 z6r?=4*5X3XWLCi`tx_mcPzQ<$ln~msF_-G-|99MYH_J%U9e$$aF`t0i6 zZ$mr6kk8b%J6mk}>_Im+1Iza~PbKSBqoy6xaneTeMiy?}TaOt>fe&fjK*G4sXJC6OqEq~;nf*Hs_JP*)&vnc_6m`tt~Sf!(neRinSu28y=C>T*~)jPZ8qyHw+D z4<(Le=8M&rpV;3}f4V^+rm5y)$A&On}{Bbp> z0U^ug98YAJubAVZeC_=VW{i=c`sNq&H+m5VEm%=)tfQ9p1_e|IQbMKJ0Y`ecKrat-iu6jgS@ z^@dY$Pp!<`nj=hpHb*MK{?A*9|HmS81v>q^JRj8*ubh_0lriAQ!DjfU6^3IQ8Mv~V7u@+^zF=ZC96s`Ids zBlxY}eod(r-c!ZDSAr7Z)9P(A(EJ;GW|-<3MPNPdzoG%7_4j3AFE%MTGxgQK1ZuEa z8pdQSV40{IXSqGIpBub3W^|5)5`-ghlQRPDJ>e@tlz z&$G1gtw{5^hwXpG6#t3Ai7L#t3Z!?~w&>H8_mSfNKAT<>z570n>dUEuKs@-Gj}cQ~ zyzpZ2t8lq3 z#Y`%e+psQ9SUM13zP^s5F+Zx$_N$w&E-}%<$BwYoooj>p8rw2j_-Ka1ta+b1PSb2= z;P&6Li%11}h-!KpVvw=bYIJi_?!^9*kj8B%LRZP(WO#$Em{H#wuk5nHb)@t4n}+aP zMQ!*0AG*!y?c>(LK&`F(s%<(>e9)!%!@JM*#wX0pidkpx`YFA9Ch{k+T?PLBt%4_3IgqFYb1IIzTmYW7mSE#1wcNnK;!*<0Rbxn0b&Xn(QiJLPgKW>f@TeF zF|oZC*A;JG?y=Gqq2s9)H+ktF0`vab?664Lwe&p^?g=dqstJ##MiVN8)!|#F-|t-x zude-Q2+>FIv-(z)*#UU_22g*3EkYy}$KE}SLL;)p(hemRYd349I&U_gEi3IODDxEc z-NKO`qJI^F-u4&?P9UHDn4=ALBma9Gr<+6#klWfRo0sQ9(ccxAcp4d$JL{?y*Bp-M z_WvGH;U)nIS&UYY^L!h^w#s#A;>p`hoSFotfk@n1Yyq8 zMd@S<8A<>`p!9#=k{a=*%>O$0cz#CfwEe=N@&y%DPi@&o)d>I1+y|S7eZ7{Qd83cP z?Lvu~`9x&oyJgs(N8LV+cR3U0fE?Gv=)2JdZ>nRDv-aCCm;);apFrPQ$8H+GS03Jf zcSYbkUo+=dL=AmhDn`Cyh_624`XCJ{Hk>4LAcI_%j(qy>X2<KiH%(k z6n6S9u!6FkuQN6c&_+Y#;Gib&33Zr*wYKcLy*`pXQV@eJS%e6b#8NO zK_XMB&GF*C&qDkO-*_7~0H0IQ0e+Au;YQ`v8SN}7UG&pj3O1*sk>n#KW~r`bOdwOM z27)kIR*PAD*I$?hz3?&I(@2XUJ>oI+Qnkp|NbM2}G*{lI;=rs4$iJt>qxa&FR>!=oVIqJ_AJ6dpcD@4#NJGfd%7BW&n;QXxGvcDn&HTr9I93z zO;%Fw-M10EVSRQ=wXN0ChSi>3Zjq^S9J*7-Z5uKT3L=_8hM#)vArr(K1^G+9WX!{RR9@Mb*>6F>6#ZNifo*Y=*AgEFCPaSmYKDmoC35Ml z#2+F_c3gDn+4A`C@4SaE(J1kob+CTtbv%5i!nuG*B0>CX%K@*tdZ&x%k90@#YU6(# z0ePmwN-bal0l8?ilP6{H6@vtIt%~d!9KHd&(VTYL5ic?VJ~GfqbIzZ^LjgSpwA`9F zyJhuKa->4BLEST4)Wx(JTk+9=%mXTcQSh&%@00tN6aFZz&Gbg_Jc3P#|135Y+$&$&Jsj`zQ+7;Ik&ld9d?y(xW!Hd4{XvbH9$3<^{lENK%bWr(cRkg z;{+n6rja7mWixNgKm8o5JyPW$nRNoI0Cmbz`J4?40EP911S!21bccficyx+eu_ zx+hx*wyswkT!3DBA}jad#`Nsf-{;rsC&i4(IO>!Z-qNWzUrQLG0&&vlQKbT1Ne1=JDHktieWa+BC*P%1TfIb94r%mHA)FY~bsb89%CNs!ry9RSEaW>oAzI;dnbV zj!j+--F$zj)w3UTQWhduc%8;v?A#!ohU7knlTUOT_)+pd4fX%N>EoUl_}TEFSkXUu zv0vSFm#6n1pl$$@dL1ya&zQSv z$#w7o76(3VM+baJ0KM@n>A7B#GeAKpLA{fXQ{4(`04{60nX2*2Sm%tb42p#zHC3uC zA{v+mgKUc}Adr@pCx={+?bpmY)T^}?OnCh=5J#NBNUIV5WbBp?%l>H1*QL&mv0|%+c7J{c4`IcE*q*SsUObeENI~|i8DzT=z;>bfOvJ|1 zp&Cz>bh(iCsi7OU1lT71e-_&R=37xnSSC8?e;4ucVm~KdsIk_5kuf&B;otMo^VAfc z#7$F|Pz36|_Kk(Mi7)x&CbA=oKqCoos^kxIjoO~~R`CMZVal*#kHfsd3fL*9FP?NF z_8dcut)6JL1eH(s!tIgIZ-!n8FpJ;#HIa6bHzb(_o|VHAYTsTH5r^ez zJQ5hsV{5HbY8x(PMYlqA^7kwg>w40OMy7SYawWWa{}+vfP?MFo1Mu=|E=OF_N@b$h z_w>9#$W>G<#v1^>1*0RzVMItsb$|Rsw$D!b;^=efr!+_U^dD1xRJP#2B_El=E;M5|2i~NhXkVLij!A&ocA}!Bn`|p72M#pAwmelRP z%khY+eL@T=M_?M!Y7($jSTvf>RuhpeJw3xyIN9uvy6hlyu=ZYYE%2K#PaoLWJngX> zo3obthD$4Ea;A>}>EN}EO*s<_bj#_b&p@I1c+{UPP_ys*q286%{ukH!VW4pl8TaeD zU?0J+eKTM8v@lg%gsp3zGP_F;7O+Ji@L2LxtP(&!P;j)a;3^?Pr^tl?(E_89i|DL1}5=^uf@48}Qfbwma$# z`uujErM8fr!PyRCR%n5NBv8Cn?L?<@DDX`8kTSNeA>mu`Z(L+u3n|_nP6~mOrabc} zDY(CuIsXg3qXG+Fa5&~q+|&jWmnNGFH$Jh{id0nan(h59T~iw&?|Om_2o~e+4=GyM zKkn)4_Q9JQmnKpj0u5^%v;F0?&sSMU`_an|?+ghb1D`5ARRDL?KhZ5JTC)x^)U>I2 z4TOidC-?oz9-12RY$H`a^9$o>CopgHQlj+%2cry9D+Z$%TLlSn9B^W)?;a=nR zA3&P1v{%q03@YqLSq4=PL6(2b)?jxPofg272Vm~6!p%8zE&ZWW??j7J$d2~M9 z6SPV`K9u7ltpHDRBOc}2>)%5JZ;Ldt5r*J0H{Bct>;z;ePVk_MUaO{2vf3G?m_aFvfVQ-)_bICmTWk-7fPw%0}p}fkzM{4m}qh%sB?FUG>J^l-E%?IN8oDX;TyoY z9Gksg{Q_Wk`zdYvkFa5Ed^eA$&gRXJ#I%RwNCK!nV?R`X$ccgmD{LUm8upt`cq^&z zE-NGw0BbgLJC#Qv5VC!4bFkrbEAZ1ozhtsi$VI-fOoUx=t|(=Tiu#p)I`=eU+&%^V zt1VAh)TzJn79|-V^MDwYY1d-V_}hHBtony(1?aE+xG|`wuZ4eb=+qqVx1u4~km-5kD3y6a$NRR%)=?pZ;>y_+$ z=+gG{IrJ2q{py@fEUb%)_}O0!M-afLwSJ`7)DEl)+bsY*{Uv|FVM!#bUtp7f^{v*p zk0PfvmP;vtd|HPNPu(_mBAM$`_hM5WX8oMbS~0)Uie#|*gVt1q6?(ACn<%}F|Bg+5 zd-fizTaQ61YTCdTLdYM~+RWaj=?mX8o?^Pud zgg~mGw4M3I%T$Fjq_q*5 z>$E9d_Yk{5%=3yF3ITHg-rt$8L^^m^$e@EnC(#=3Eqvx)*3a-OOWI|=LKpn=USDI~ zlPyJuw$+S)?k*zaW1HQ330hpmR0$yUCq4ZR=IJkd(g?gL?I-MZxGKrhHBOs&8yRrT z1;8n*sT?PS?#k@;POWiS8P)tudo6V*ApBK>BYs=ELcG0SK!gP9SztDMol?`suT4lx@aq8ue^Br{mJhHcl)z8Jp;_YRmHj0OI;`*<~^Ic*0#= zuL9?>9_)&J`;_hr(c#U4l*ifdmv1>kfknbKxuW z?0v_JSq)~NOomTO zSka0AB!^|jbX{O_W*QVRJFT4zs8moaP&B&%38cU*2)K~px2Dfd($WC#Tu4cH?upv& zHY9rAqWbead~CNKHEht4T*NGzB5b($B}T~WMtO#P=z4BF{OLB%$moxN!!sKC0*uZ zqWahe!dVm@%;BIM^Mvdjtfl17HF$d<#r1zo?J;GO^!~oWrzaE~aCjKCXOno8f?^{x`zDgTJ{|W31EZEu9lAb3LnbLhrR0llZ^KD!*6L_ac z#khO+=u6JfaNODii#{`9%CRuMcX2dP*(y(MJCxSt`M-`{pm8k%37tR|saCrelV78MX6_VQ6{C1>r%7Sz&P7;8pmR@J0iuT^`rm&Y~R2J3klSa<{KjQO3B z?&u(%J3>$&EI7a?dF;1YI2gv`Y4{066yz594|gDOh<`W|lacs-AWd09rs%sM3P5I6 zAgrh+Ef1^15RD{Oz>lYEI{oC=aLnbN6o>^~If9E?=+l>)p%GhK8%E46P5qj}gj~E@ z`*rC)?H79EkJ}3w&lh*$|K?SHO9>YBw^Rb83f+5a^ja`-sqB8Uc*PIAe6OR1P3A8* zMD_xWvQ<4qtmCf**Q`{iLl}@PNfHGr(x(~aijjIzM%s~K``Kjy>0Qf(*GbgbmrTmK zGk7Obbj92#6O#ZteXc|i&v+jRyM)40|dI<-LJi?>Mk^%j$Z@~ z#Y+m!l|=UlEdszBT9Q6K+3FFpYKIbu3FMGf*qZ@D z-<@}!(mY*sZ~AIiLwkuwTjbU>KW|8Ol*O|@WA!ISeL@|^d;IJ=+F^?LUvs)(ZolwU zQNZMjDFzMT4etk2gVdQ9mIBitQJ^N21P%A%jbav$Km!q30$rNzdM6P45+>}}=(gWc zg0~>wy;>>Y;Ln(cy(S6zplsn(7M=NRV50%Prdg{)g6KOlA7IGZ*+9DkQ6RvS0*ZDJ zvs0u2vIi`Hv<6imwy&yzNmbpzygaw>lpD&`)LAp^PQUW$t~IZB(&?MZA;m`mefMVS$|#tyVn1Bdf!wu^u&gJLM^9hPWh+n7{YueI z(R&4X$g==;X?Nke=LvfJ4KWo7Hn7XfqsE)H{rAE(8DfwX#r-~AJWWe0*(`(f;|W~0 zeJs9(2BC*mo{!h>oMU!ycdhq8)~P@2M!9Hg@zea9_*$WEOuNYYFuVqq^m2I+>FKUI>z5vb9?ol7fLgLrL7p#P7LN^Mr5T0p z`CRRKfWYLGJHSKmUarycB;aPcqC%h-I>6e@*l`OtIG2<>Z6{JxCzG|8Dil)drC^Ik zJ6?XvkuB(H3x|ppOkvTyPdZk<6lvt`+1%9bagvAO zy&(iW2Csng8s^|a8)2&vQ5YN6KtdBbOD(%}F)c9!x!6_BlwHBnxmo3C>=BND!NNau z###Fk@)0Oy^=no6Cyz8kEgulQ*y3oTMB)cMS43JI-D?fxp1FFtJve6V;Y}UHUk&m`-qQpKFjGy`Uu9ujh^4*xxwUu&7 zMceJ7s;>AUhgkHPj8BQIN)OQ6Ih-hVOc+*O4%U!|vAZ9=K-hwLjH;|IAA6Za%9nJ_ zX(lanpd6mWHcC^yvAh7x?woh8Pxz=Ds;%>Jd=@~TPJlO(?l+2V4>-&g$ozmt2OOl* zQA!Epqu>`TcH|76{ooN{Bz;+L2#;?^eu*Czctbd-p}Hp>gh;K;|HUdWKWq8%zbDVwJtoOvl8< zA!a84dh{Sgdi>_%0B?Bcds3^+SMURK?g}$A_7?Ys-aq>ToXezKyqlO+imc*d7+_B3 zKEru+M*E0T8NDj*I$M=I+_PXR{@88~>}_*^ z3Bg&jA6WL#(mw%wpWDF)kyarKGqr%@ z>`b?z{{GzQVC|1=NBPU9>c%-sAzlpUX<(5aqu2!jWZdoZ`Q5z3xlFGz-b%$BXwL)$E`Mp69iPp@FZU!wJw>}5a zL7dW@cctLk%~*AGqRTeG$UZuE#RvJ|kZU9hp4<^`OxBg7VmT=#mwu`jDbgb$xX9`w zv3-RTpC*_^z6->N0NK_}4{(ez6MBsV%Y~u{R2}t8Kuy0@&vz8>1k1Zxh27@uy`L}I zvk|_WN!kNwi82*YG&a6^_mhX4jXV}$C9js_6X|HvAHCa`dl<{n!md*TQ0uB4zB5n< z`m{D)|GC)mZ|coN$|e|$8FN;AVHoj^{as7mez)-b5m4QO0-GwNgBTeScEC+RA#isA zo4a3(033{Xr+tr7&LrXjVrw<~5`SNNJtfxVf;H4Y=r3#iXUPc$rd|2h4E^UZ6os7< zpi&2}1X%U;wz1=!WZ()p)z*VFNGjOJmLwuvySANot$LV$IOPFWDceB=tWShebcb~& zdlew>fO1WncwpJl=T&rRGQM|;D!EI8b{C35!pNV?i(ZnwDV|_!^m$oo?&cA=i?7Pz z07UEpb&PDCi?VL|Cla97e!qo88gH$DLY9qh{pqKw#on*wferKrh7%<1KZS57dC8`J z10;mNW@$GF(g*czdJB+&n+zV`TM3kP^1$Pzm<5~^7ULJNqd?hjJ07~V``i|;tpl5` zx_kPa9uDysQmzPs>5D!Dc#`y~(0yz#{zc$Gk8La1cn<_SJUZ3MSX5CKqD85D7)7$7 zGkCbfk%0joK8LCJIu{@*a-`t$cN3tWDVZZIqFFZ+`9?^}B(;Bbqiamz(h&LVHybI7d2U4h!l zkmt&h(8cdS7^DJ+OJRkFOPf5S1P()L&PEZ$!HUV8(vO93tJfH*qz`5IllMGH_CLo0 zcWCRpU6@Y^kK{+1R82CSnsjaBqULa1G^sN?ZwH{*J*|ufxGvf|gIQ9kInQB=49Phn zI(A$fDR%I03J}ZmR<_+Q9J0=vcP*pPbtGE=F*JUr#*Z0O`&f$7wBK;P{)vSHuHuQg z%^JcBWNRj((oQ=Fz<=~sp_vHBy(`XNT#}(Q2BdCJ*Rfy^1l;x<8(xj0eZ)j9?B7jE zuaxNFsZFj~sY9VH~~EFN^>@t z{oL;WgxuQ}`qCUY+e3hGQAFs3i(LWvXCBroL*Nv&ho>MLU_|Q4f7Ts#eq-lN>O@;S6YmxZ5ueI4s;3VUK&cWK6e@U9aZDt4v&B%`^$L*C4V708#tZ!hu7e zo+6C-;q*?YkK3sjA6Mt1u+gnu@G-}8sR_`_?t#J2Y~e^JV-%woJ*&2le(mtG?qS{1 z)}9T@N2=p&^jp^B2T1Gylj_@o%xC4I)9Rv?Ii&f;w)H*3eQox83D7#@r;^0I`@Jir zk6QE3VSyqN#O-Op1HjtfW=9(g2LbMvixKR~L}4dVI9s8P16pmjeO^Fg@yufz%Cq0A z{t!aOGgH*Z^_SF=vlUA)WVsz2W*(1fAcF&uj^@wLKg~bq>F=mCfaCCJpRqHBduAU} z3h&45>)o-mJbi!e(|DB=unZz%H*+UE-1<_DI(YUe5-{I0TX@W1UC_eDQR@J31N0!t zIP`3xWu0^PQ^FJOEbo8Z+pY0)h(r7==;+d3tf?0{K4k`U69B;#y zsPjf@3osLF{8#Ab7KJiNA%7m@*k4?s#?5?l^eG`mb|X*=a@(TW&x~w?i#xljVUMBK z*5?VMSwDEUru4uWk!WYT4R;~9ZyOuvC__f}c;&!ktmg}1=BN55JF)?~58<_8jU&=C zLn(Cz77|7QR6+i9guttD)cXs>)EGiMgdZk>OUFc{0kPJI!K$?nr}Bl$(nJPM>qy>! zOA%1?f<)3Q`CMcu2$y}_L+8=ls0-%T;6ar56~WHv;nl zP*qOCoERftCisO!9_EtjI(Mp~p41w5NZBs}VFE}gbiT8VxzTl3Dp3|fqh)_bmi#Uq z`R6qLD+n=}r7HbwEGf;a=DKrY^el{ESy7F&FCQsRd(+U=+!FgQbd$m0%r3Ej!(0Rv z4kR>pu~7f(aGT(5S=~e#ZRhPX;j)V*wh64;35Dj=qkrHBW5f;rRZda-y*Sp|av&zP zR{_|%NjLMB<~&|t%_qekq=;C0ipG)W6T6N(O0nM!eFw-JAbUV6B?M1G!j75^@)6L9 z>}-IOq0V{aQL#^ab$ha{da^zCf%utqakZ*c+e}U%0JL{K6wnSoXlgtnFGla>LG** z(y|iMR7Tsc12DWm#)0c>t!R&zLk#~Tt$e4PWcK-kV0PUz0=5WWB8p#fgi1=~*Yilm zRuh$L!*=w3MUrUC@Et4Hi8osfIx;`r?k(GGZEW6lFzqmGXb&tb%^GMVNhUgLD z0L4|r#0m3Du+g!8{K&o8TVH*!X?*yLddqcG!&83k-nynYT|mM;`+7$GYLAK2vg%ew zNBu?7PcT)`f9~xQXRV69b4AB&)#Sy-`ID$FzLNBnu2Jt^ALBJI2)!yj*Jqjh7)aW_o;r1h> zh2bKEjkp7toar7;vpRP0VR@>s;+fB%!=JIISHkGSW=W{r|5_k=1>i+CgGEYv!u@6{ zwI6N|!#lDCOy>&lFlf#!06sHxa;`?kEGcE2?IpodT0Vn7|V&H&?}ab%r2CenC504Rg+)`0&l zS9lh5m1Y~I*a>%g@N{(*jcetku_z2L(LfSJSiob%v@b_kJOEFx9-mS?td^7N)(&|- z41r+fs!)=uu$Ah2=hu@9i7i+vb^EXWSo?Y&fZa7(ElU_k4(8rI=U-?DLr$Bz4+gPe z_pOgX_CR>znYUz93E_0dnT<+a6cd5Vhc{4;pOC;D9bhNoRR%}U8^VOm+}EkD7|XK+ zXD?g9KF;$FWWv<=i{%yvecjnY1_6X@3ua<#XTnc9}&hYn+ zjh-It=KAKL6J`+N);|qw2>V~om-fM?!t)wz6KT!8JQ#&>xFFQ#K4;F~0;j!N&Rz;< zkWThMS-ZHZa%a8q7?EhImP-AY7R=$i_`?>gN3mT1YN3jUAWZ=+Za^|56*N}O-I7)= zulLVLzn@#2G&R4(Sb|!IeqX5pLIU95nyxHj0|uty2U2B_fF}4~ede zWEv)|?z8P|?_=t;)ic=DUf#oC(IAMAmO2_9u$ZMt0EV-X1<#24jq{T%irb=x{+ov- zn>%M8>6nAK+yB~s+57#gq=viehx4S|TkD;p=D~ZH2M$HjS)rtdr2B)p51p!>ODhjo zFzD^o_i+V&svr=R%g6G)RUQocmz(JeDk1~HnsEZIa|tL-7HqicOy(@cefHE&mONK% zC*pEK0G(37hrP+W5C+x3;g{#>s(tY+92XMXZBf9@8D%D%IFxxrtD7(cF?2^CtVs6P zxA6eahUDY=8N?o?0@ph`M4}B0s@oDtUHp|p5onLT_U$1(VzB6tJ%qmq2}}mo_yAeL z=C1TC-^{1-Y9n^s<-MuHRWO`QTp5KYrUP5IZ?#zI4so(Pf8TMd1v$O;V0?7y05A{O z-C(rDX@g-Oz_88CU_Gvoz@}RS{=AQ6k<(nE4z+i0FNew9YWh3aDVs*O!cqRs(5xk-(2s(?qZg<)&y9dqe)N_d&%O( z1%SYLWNO>zoi4<-(Q5F6vi2zzWA$p~QHxc=WOafRiFz51-<7Qhpokt?UIOMlo_vmu$;u6=y!IzW$Y)D=9|CS%2o-6cL zMUJIMQ*=D|2W4K_en_4_iNtwG8`d6fDrKLcK2cu@#-M@s$1h)TPy>%_Eh$|G$1|Av+zec-? z%a~miQzepO7;>cf$<$X6Ac!l3XCy%`tB}Lyb7kv=ewi(bq1wtAS8%+aDes8Tf%YupT#suIKtmA8W=-2Q>oLPRnki7d45$G~fPi_>rXf((ey1+0)dN zuKA(Meta>3K9o9R!tS%b*=+Nplw55+>2}jdrb59^)HNyVL4GeIxh~M??F=x)d!IUt z1P@CGW%F@0e}ISdv1>OGP798@+{iUF5b0w;6r{EEiS=1cH1ustQ{b(%__q6UG{39x z*ZD*vpE2i#a_c#0YcQwi1lvg7FekkT{L48qn)359O@@A_XN8iL^SoWKD#<&nJ40D`RQWYMvavE}) z^r~2O*Y&4Dc&BHbmv4)snA_icO_>wF_lB~Dgz6$?WjRY3SkpKdXI4e!+bKap*gEY+ z8z=F>4LQ#8Rkic7|2n_~39U?(9aS4TQ9gbD>kV#(G+#^&R8?CxDpDJg%qh#M(@U7~ z)6vdgZ1T61uI2+P5)y><6|WO(>=G{<`O#VX?VTMPd5(aqO$5IYt+~@Mb!Y%mA)|#_ zmgtFb@qCeGBCE!i8s{`5uC1p94YjVfeOK=vw4BWiNcVu3O@<{&&;9E2Zd0Mw!pVRc zLT2_G&cok?f~npcb(6+CCjm$ax!f7RN!Qo?-BaWB+kdz03?kI5#^H&Ppv<__ZWEj& zant>ml=*5-nzC!7;Y5CfVyXj*-81a-uDq?#Z}8u;4b0pa+|D%(JXJO2;eD>Wz8kor z!PaJ5g1#qCDm5Fjp2M?$;d>E9jzD@ zjC?2q9&t+b^gEK(B^o$lO|1EPC>R&M9mwsjh$oilqfJlO2DrOnEwqXlF{eV!`y5JQ zt7E4yGbo0*XEbb{J-p3}HyW#hivm`2&|!#L^KYu&%C8)C9pWL7bn!#|%3y z6-wE?n+oeJ9uqA-VlwNU^z!>SlW?CgK87YIP0ncplZg*Iv$pzJn+SE;o6WlCa=X7i zpQA}Fs4GaS8cj_M1V%=gwtr*1=Kl-D^>nt}Pj5^ufd)IcB1t=ZMDdo_yt;3nzGjAO;}{ zu%^$+PuY&$Xp+A1p`LP9cPLG6V+|1Pf%M12)4V=zj<_G{+z)&{OLH#4Iym-j!sUyY zm=)$;au?g| z#n$3Q9KsuRh8ps|_{ZB}UHUA|LpGHu4=t`dPM!;8^JVS^_V*kMHALVu|=Fq(7t_P<+^0sKUi07Tkyd$m| z?MWC8S4=|flyi#M>UYclG0rZ31b0z(g3xUw9S^yeNkF>$eg^+`~N5A39=jp_F*1*lq3VvADi|(QvfGLn+VIf(z<4 zbIW;CG?w4>Zt2G?ilfr_iDH&qZE9)C5k6BJvusx2G7pcBz$T&=L0OuPH*8{jeJ+u9 z)7kF&4fl%cxhjO-^zZ1o47K%>*ntAGoF+XNx$GL0ijjhTWcB{|5q6%ocRjIg!N?=Q z+#e`GrKiVNAHYFr&+g-_pg3F|BYe4)b07fQVQJ*x7?q&8#jc{+SP#?{%CUW4lTFjerDV(9GD1#tQUEYqEQOOyP~+>JrFG?ou0SYji)8)_K+hg*K4YJG8~ecsH- zFg@M=0~$I*hNh^}2rtuz^f4)fnwO?U{cybje^`q#)_H|iFV5OY#^7X;h(c-h14W}8 z*fl5{Q{xLARNXguj^%}!Dw`=W+PND8(}`ZseE0xK1~Yy^ZcG7c%(moJ zUuPrV*&N|tET+67UC-r-`No6iMYUU53vDQM$i%bIz^A%hdt(oNuM^*z3mz1k-XEp~ zqEEO;Jh+E9V|~NuX@Z;FNtRDVQhQ$$@A7?5p@M4KA|+%q`Pd7G+SWTlNWVCmDV;^0 z=Dl9G%qwyv{k9DF3Rm zPLjWO@AR%)JK?9=NIt!Ow1I@~Ztxk|RIl%C>`ARa2As4mUi;P0u@B6~h5cJU80pyE z6`7lnVn6zPMELh^OKj3-p+JodlIJg;k0aLqYpyrl%qywFBwSl(zbs~8@N<*z1wHyR zJZV%#2T7WrW5yZd9q?Ybmy44kyG_hei!$a_lO~>&7kqceMLX# zFeAAy?eA9WBCgC^`@&W=h3YhoW{EgPcfVXZENyOjkP&FDeLd_Fqs!;=KyhC}gzgdE z99GmwXzy`$@dDhN^gm;SZm+IBW(jsR`C!!4-CK?z&=+?^RES1bWaawG22xj~d64Ku z);J@Cw7bS)KgqU+Brms^+c6lZ1PtP;Mt)GLdD)4z&mF}!&N68!9zxtmL`H!Ua;hjM z{a(aT(7{dWo$<+t%(v~I16WZCOdm#r?E_82c*kRU0tF*BQ|7dvCvXs|pN_Jw8gcXU zCRMYGJNXbH6(o~_GL*RLrg#$H1koySnf2@0 zk;bzWcFHxlTDJ4A%bKYKKa!ON}DMV~0zfIj0hwCl;2*p&bimA6BRsic1S+dsrq zLh;b_u~etp@ZlhS8wy-@Isv}E>aplo<#iIA&ws|ESfKgt%($2nX(|o}vky5MhM!@e zXc{i^FQ$W3yc87hb0o$vBsq+ns4XE7B=kQDW|l9YcSrpvtI3Yo4g>LpeYmG>g{!@d0RgqC7D*~3fu zKs6})C!8|v*v#Lpw&)Lj(GIz8MexuAV}5NPS|{s363(|-_Tj0%O#KSdU0aP0$7ePI z4rzOZ=)r+>upM#i`=KJxVz??+f-9w1`2mK_H96Jrs>pL&Z%#P#-|OE@{7&P|^HEx5 zK7XH|!zTWN)~ZD2^32T)g09Lt2x8h@2Nc`rAuof2QJFsEMwha= z{~4;@K4wE+$#c)7kF&gVn275}Q($uJmy4HxBR7Ixq86S@Muj?tZtv-Cx7P0P`K7wF zV<%(plP~wpU43y@4dfL+2)v59QSuAp|x3v4+gLt`Fhgr_s)sqy2#hd30 zrV!a#ld2KM+bjB#DVdOoz#$y1rYz0(hQO-+fSxi+Ng|*uUS2O=$-y^NDzl9J`Eyp2 zFYtuN9PoNyzd>+{^NG-d1#$g0l2>CF2l(NpPgCJ#wmzKsY_B`?aEmvS#ZEqZie&aD z!@+@E!_&NgzVY<_J#$i>?Z^E;KOOhC7OwJc?voy-J&cOp94r`|A%CAg7+a8efB3?u z{6`g(_u`fTn;rbcN)a}0g3PN7|HcYG581!RS&N5ldw<&3EPJC15h4v5nrils*<}R1 zir)g{OC~nTQ}bVPd~qQ+{6f`wIFJMnxm0*j0t;E8#Ai^1w13dNV^TAjTRq#06tuYRt@47}@QVA6}iC+12>8-PTo0;PW84=a5R76^J=^rg5L(NkWJtw+xyg^Xw7_@&FC^tnTWiFjIG%&-g)EPcXAzfWsl{E{AV6P{(+zj_6rIV1b!4Vzyk?19ZU#J0sM9MOxY;P0odxCUuS=+XCJ7)*q9Cq zgRjWUl16WS41DwVieUUi-Tv^gwDm2wozS-~cFfHePKu$?dk%Cn*C?#}UM&y7Bpz9FA0oep;m}1nnX*# zvqa%0zjUihtM&y6piZvOcrN)TB0y%t2p&1n!h%qECE29lpS9uC+{=ev70)k&JdH-W=>6b*jtn zHmRkT2QD2Te6LgYvsFOz()b+K3s3Fk%IVPpedIc53;z!nxcN^pGUU0tRdU`*n|bh< zG)6_kyrcnN>c7P8<68c}pV+AIeR&ocXb+yx-(MlNZxt+nYequsCz0n^ z?Y)&lLmKTe83?+*vhq?GjS@1x0o9Z_{&=edK{RtX7#*kA(tOC#Y17hE{MP87Whu#v zE;V}@lQQ_H(Vy~9Z(d+cOm?o^aRp(9&OE(VM9A=~2{F(5mV}t}Hcp?BY{}$?NAEUA z70c$rvBA2ARjU5m%y`p)`=sSV5>qc!|{ZXQ#N?0(axyVcHK##>5^JBB1k6ZJep-1U9tX#9@MkcLhJaZ}9(G3#AoqhSI%FB?`G0+s z{NtzvsSdjC9o$s$$3iv^xv|UgUFgo1BW;mBF=M8peoVzLlI4}!YD8=e9(lZ~4weh43{VZCQL0lMgMzX=TvYeMh zw2HUNB`z8f;a@86Ufp$7kNTZ0%E(w-ntD zS(SbhWm)HUMi!oId{g5{o+XBQS1EolZiQi@Sx@C*g1lL^!zstZDYe~2YtA=V_J$@q zJ6PTiP82U0o_rJP{jdrz>L|=PpT{?Ex_Cs#VA*q}Na%GJ4->yG8a)rQQyA+GFg+(& z`?@4~L(53R^LO9ONNs_mp>`5e7%2@5jydOF9`8CdPSIdUvssg3wP-DlM#L;+D34N< zgvPE>B)ONLRv~KN_-SZ4tULNc;G1$G;&7_NVoqEB{2HObcXmH>qij5Ku%}W@ z(q%6c|AFxqKKsC)D(3?SiiCD)Os6N^dr66=nrJL!-*3wtaU|iE18>+P8Q8!ehZ5}Q z9;}mt=B@Wc*Xn8B^|LEEn?`FX{pPfnw>VM*kea7u`^zU-*D{B)Q#f6AD6anixMe1= zV-Q8p&f4gFl{!mbBCLpJU`DVP)8$j;4cUoS@7+|Y{UhqTOC#F&fIiM)!1ovFUc zzs@r_%H+I`u|*Mzwd(M7f0bHT=5`~PZ&#W|JEP+P(a^Budc1OeMO9~?V}4JCs6b;} zPg}@4*uuT}4>Ju1P;?w_pG9BumhfdJdFqwVApxh0aPF{M3otL((idCDewGJ?Sm#96Wf{MT#C&(6fX7vW#lnJ*?UAEXR!HzcLSugAExq`_d&y~_?BrFMB(TdT z^Q_Gf!x=O)^_Cdodr-Y}Hfa0|D~^BhX_TW;tCo}mf#fjMfgvkpj2&K6j=h6vI8?~Y zuIO!;8F9E}VM+?4g;esPG!4}E?MMg$AbV0^06#b6Tfx>*0^`Vwzh!Z$?8Rv#k5?v2 z_youcj@g!`jYG8}tc?uC$v`CM5a}Vl& zzO65o2WXMVN+cbB+G%apd*d+-{KFz*Pg_{EeTH8f2F`U9Hi7~<^A!nm_ zhwIB8OO*zMIqS0$hKgz*T{ql zjDILj+xeB9vB=Z1OrvGeT>3hS1c|sh22@Q3Rmx07Cb%mTmnt_8Eh&s~Zgkw@V1ie~ z$i__88Y@U-(C{T5NRJOYop_@g&ZzhUx~~eF``wSO0iG|vR-WLiZU++DOl9@*5frxW zewBN|!tTutzZqtPUYxJi3s*fdV||CKd`CtjvYp|2)QxBB#pmD!eQ55_Z@cw3h+Dd5 zG8eh1UCnvyY7TCh**Ps2bv@adUE3ma&HFB^pX}_7<74>b^t#6@k%M61;f~}ax7G2v zt~d-MJGsQH-VvEco^*!bv?>K;%bJ_zOpeD?lmo<3D&gVvGnIWbXa(#Oy@_bIvM=v`7gcqWJX1PbC;@*=;y~PN`KsR|#3^m}C8;WMT-2h~ zxnD1nOhE{2pDpIG-S_ME{l1Sppi421FpmpgRK<*ayU&gp!~=TB2&OcCmq##_hfI(w zvAX45Bk`|<#fojYb+*r41Uvyzx~WvTL_61cMe(sjc{_1gamgQR&?&I9-!^?XijlM0 z{=nPl!6tNc7LBp$!PR72BFO?mPugnmwUy{H>Sl*)e!s41sMsab#z2%lA0wlkEK@wQVd;zsP3cdGVa#eq#7SIBgMhn~Twu z^Rh0B;0gm?);W4L?Ckld4HEv=6ZW1iyP3pz6Y*yZ36nV8<9Phl!>+JgVmL~Mx1sU? z4CBK5P5aJT{ZFGD0aT2v-0w}M-;G~0JvqpeuOsLwa$qwOSluE9M7|p?(LYd1LJ8(|Nf*G?Q&_YT(F%BllWK>b} z8~uz#-22ylaLm}A8Dad_1PcM~|7a`$|!xam9y;HSt zva(xP_8%uM9_Y}|yh&sU6P9+?O*iyy2qfgKI z7G)m%d+$MT9Q9A00!2jumG-hAj+p=NB1 zm5bEZ@3*?-o-o_j#&?5ge-DBHx#7^}%&mYZ*R{XY;38a;B>rUydbpdMYWSJ2eLz9< zW19p$Sxx!w~06i=(@a54n%CY&524^xF?k zVIE+lZ!ILQlKb;a1c;K6A;3h__QFSfKH-PS5VA8{=qHAqWmZT7$}{M5R>4Kj-U&bY z?SF~oB%vcqlo+d1_8)#JHzgoqxv}6Id5I5xt^5re=MeuuydwIsuZ#39LU%BX5dQLp zh5DUBnRWLjs?BQsC*E%w$^k8RFG>stdV=W=Kn)hTyt3^UYF2;jyXzE=e(JSV@ILzM z@Oo#fvue6>e6;3jwjLD#m|dHb7)=!~tKVBSeYPht>NXxGHn!Zq(d<0MAbTHImKL{% z)UWzF2Gptrm3CoT=YV%RLwST#Y~UF{Iz)T*y1XL}FE6g+%#Y}@>|do=2S{rvC1rPF z%*%=!#oHYnp7upV@Il~ZOGJyOtARH55n%QxHG88YM%5IBGHa?XYxV{WHM=k#hgIso zVfo}?P83Q?lBvty<8A+}LT>6Lw1+KTr5@W=JmS+UtRC%lt&FzSDjEeIC({&o>yKnV z>oBe5LK7H2j_Co_KZZN;jU39PBe(8=LB_mk5YMKECWjdD@lixC<|!unEjv}THX$3R zuVX!iqXiqI*2T%eYD#%}?0uP%LoC9G*9=IT3J5-{X7-j<=b{1h@7UGp)0YV#VRmgvw7K?J z$(CnEabO-Re`UOlO8eVbR@4S3j$%SII#v3(W@GurBQ%O;H}zo?t0()*y6y8QaF~3(TTtAH_lBic$t?6%8jd zyrpy$fQO8=q_7PB9d!G~AI6_R9Lz#5&E4P&(#pUlp@^hN$M(ohGV$x=@%Z~$HSwWt zgs-p|oJaEtFNd0CI{V2^Vc6}&QgV7n()~Wq;b3@rCUeIMB8f`bR3`tXXrimcb{x(Z z#14N(v47AC`*hvm%8%wg`U#E=obV0zTZwQ=wf402Z>{;yl{r}$EG8-IcU>PhJ*{N8 z>o}^1GMolPEpK`?sL%*}SeZVuc)#eDwbOymL_8mDTjWE7ty@zUFR9R?u0@|ZUvIi7 zp1ck(Ya$YvS!ik7pO<^E+WT@yGohQDuKj@iVEdI{E*fg|GD}Z0g~|HY1rTz0&-cOa z%0#Uc%9~H2voRYPoC<;#ZWS$!B6VjA>k*{5$fJ}2?uN+Q((8yFc1%%0p3m%*xN(}( z5Xv{7P}$WK62I!JghC@r-sK?-+$G{%7#DDVNx) z7`ev41HQXL#1HL3J7Xd{UD3VK(M9~<$gab~ojXgx5g9{CrOahy#}C&-(AKRZB zVET<)Ur&xw+)}Z{>V3TB85of)K-)P@<9$ze+iD>(*8GFUq6d3J)`l%DfBPwH-i8{q z==vYj)|Y!))f#6nAXcX0!BN9w`pXgb-D6R-=Ol#NWzAFOe#Pl=gabZgU;)q6t_kSh#L;w8&n^$^Nkdl~h0xcYYmjhT+Qb2KLi*)XmD#M}6DJFik#Gaut%)nJp<#gkY-Ggt72{RFN*Jd5E zr=l*+hxvb?<+%R~4EYQ@#D&npWbucoI7&wA3D!UazhB-WWOVfmpU|4bq*mmbu+(_x z)Y0hxS=JJpTN6;j^i_tk2{0DHY-Wgxp+Q<1T9$Z5Y!ug5zur@ycNl5l`&PG`3coG<}`-4E*T#J5Rk%SX<57zxg4uU)@D8mg_m*nmV8B z+m?xA=%|PakZN*J`JA0SZx%&AO>ftyI~=9hb2vPcjvsqG=8&&sqCpVO8dm?b1BY%7 z8LeDdV)mCR?Yi&(ox#JFnXp;o*lIyWnc9gD{Mo-kT8?BsJDr@)MGfQRl{SLwlnQvC z$v{utt-|%kIf469gAiC1H}xmk5K+tD1OO%@~};B|ne+DezE=Uy%QdG}Hl(f&#;D(AprPwItUbkjhl|%8LGG zC$P#f(&vDK*h6CmOMzRuJ#3<25ieP*osUr*@P?@UzNn)uxKI_n&xOe?S(Y@s;kcha zp-x_^K;40zHkDJGtv|eVGM)PIDL^=~1Fnz81C5fzOscIX*yx#x`VENCb!$Q>t&Ik$ zW5lX+@YqTU4n#WtttPUj=4VY7>x3&osT>x2(ejtD7l?7bFM(pnyVZoC5pHfxzN@hE zrpHB6kL$94GjI7L1Lz0LspLY^ttD?*L3l)%>Fp;VSNE{mqf61p6j66y=Z>5Swn&v( z0VK$o4qXZXei5s{R%`sRv;PTi`*zQN=P<8$N+wf$dOe+?rfRB<1-40k=uw4}P#S&S z4Uz@uiF-7#5V)=-vC#khQ!OW8UXrG!8u%0X->|F^@#`lcA$DMby{XFY3{Li}g=Ao3 z*u(uIM#Parm(WopB*L4rANm<@M%x;v0&1lzU>&az5sQ?`_Qv6CcmY*R4j&ET;L{)I zLG3Bsyj58LlbjCwGQ&YhM-%>$xnO&itJ|TeMEd3jQt+czJLIyl(551VDFBujSOUEB zl~US>0<#5D(NnXEDP3Mu7QqK@&BAcYIYs6DMJA(XV?ell9Sg`>=|&%?`~)JdCMGsx zPD7@Q>>OGS+(mP}Lbb}TW#`*@V?%BPlc8TQOi59X)>!LlYWozM!iyVUQIBmCESh;C z57&`rPcN=Y}$?R^$%i1b)v+_dv+G)>f`_zh8*y--F23rz+>?waOiW_LKx1mYX zb=|lgkf?Vld@)d)2*hX{&4HG`=#n~lz}mAQ2!TJdzO9^gZ)btYEhhxjvmQU`*{M*3 zxM4s)bc!#1q%|5r5u&(s*Z@(VOVS?AMaHQ}%+`7;%kIdj`HVPd!0J2QR1rYkZ0Ox_ zhmt@e_dl@24zOqx|4y%+WC#E!q50J4AY$T?^J8iFc33{A+UP%Jv*-P#rH<@gjb10D zM%w!_PNn#O1_5w#A-He(e~F>6!}o{6ceB@GNPio|?woK`!_~-D8z#D{mj8R?(0Cx# zN>yK4k}QTp6Bxt5j1auX)}J6)hS+m02@gzaZAsXuP{%?+Rf49YQWF9!do3T2Z}2N~ z%O^j*?bOjSE0U64R@sR%`1lrm#?M~%el$+B)ajawq7%3yyS$DYx<2^45O9YwYNkJ& zqMaaN)?M9T{&X=ro?ZLXuK5aOlH_cWK7X@#LxOx_w`X6W=@3YwMSxZAVz#=h^X1Pk zl@6v|>duL~%$SibOP$28*@sr<2WqVU6`{rUy#LioJ&Di^pYuG?^Rp`~Dkm8WgbnPM zQmyatR@nY}^}O%R+n&0p20R26Oi}7LZR!7geN9@x`{FuQrAD3?H}N#00_?f`$=qrR zin(YGjKRgKc;L4)@8tyQ?Jvc>p+RU5;#&c&^~YB*DZejPRk*pcjyH>X-q2o%nj8w1 zS|a{8?z9ZuxyxQc=1=q>qjbQp#ylg(-NYGT>oD#B7LR*mkDm0U=0->7O#yZDAEYc-0bt~8S6#- zDtoC0C9NzWH1FkRS?K%S-kS3+o}t4I<}fYGe_=J{w1;^two+_c|MsV|-TNS+Zvm&d zq`)q<4_>Nyv8DwlILu^nQm<~HWa_fffJ`U}qbUAHxRo_j=p^Y5jG1 zOw}tX5!_2GnD~`NXI)fI;~apWnW4EW*J1NRip|iH89DF&(sU5BnIRY(HAG*amKZKq zkk-s72M+v6o}Jcw<1idtf~(K!fH49>jysg1aj+2)ZI0yE7$$<35Ef_ZQA0qMoQ^;X z18O!PQ(EAui6BxAZdTZpt%0Fk~`&7I!uNx?YwFOm`hd9Ux+aZZ@G1w>kBh zjv@RCM?-WQed)|n+h!-R9_LK3LJa64ZaTqc`Vt*_S zR6vx`Wm(7E%(ES`cBT6E=9!C>Nc zakXX{hw(Ls?76qTAM4b)gYPJ_My%C--`snEU>!us^?Vm`ih#Vt@6;Gu=tqmOKD!pn z^MA}$?u+#i1{ELSDF+4lOix)V=R)b~dO@c>#HN5b-afM`Jy@%v^_& zun1p2lGK<4ZnEo4=nHf=ggaEi-+Y)9;i~ZpJialpUZ+u)k>#h{i|ExoUnUjBw z10Yj@?vejc4Go^-2{vlg4Y10Jc+{7tm#XPK;Bpk{!~$u7QT6T-y;bvI*#NZw7!2RV zfc|{-3+OAO%M^dO+kP~l$yhq}-y+%dcRs+xvRgoies#?Z)Yc@GHj0wcBgC_pQ}Jf2RJrbY;s~zEDN;(=S_37+hIpoT$X!x6@< zQ^N~zWr4m1EE^FW0YJP=rM%J7h&s7s6%y4YCW-yFJi7>y33&9Zo4IqfT^i?rqeC!K zSwg2p;I}I7{_6bAN3IC)RLUISM0b;*1{6a7$P|EDTYUW)_fJIjH4tCl7gxg)d{t{5`Tqs91p8d>nf4I}NT!oF%i+OUu-&D@TxbUIA>%@Qu zzqTQ1KtDs%K}zl9ov-#swwWcDeleDHM)O`g%R(J*C_ligD1NoXJPCsi3<9(ibtD{u z85~SiWz_T?sTz`+!J(U&%sE`Y0vRT-hKy}ZTNg6vt5*&`1mg@_z@Y} zUUVgiATj#}?a%%F&hoOHH?*D#!oI;ZQCyz_;)`uuyNx?>3)fEm;fe|K*!%hzP z{dyCc0tvbJ$}5>EVq5{Zhjgw>al*<=a@;t>?j3a+J|jkkN;P}us|%g_Nn3LScZ+`b zT>GYW1&LM+=m{$V%qt|v$wa&)3pWr1-RZXi+f~NA?IXu|oz7O{A{WN$ciYGO(N6^w z+-T=c?3TVmxdFT_=OGjh-d8)CET_aXMzE3D-mLrq=Bt8$j;T4WH#o|?x2GA-NRM_zocB*nj>JtKZU5@Z!X*F*D#!c{=71~Z*J*&o zJuuG9n$}X`xY>32C$Yv8CwbCn46p=oN^=2kiou6~`0!oB$$G~L@}q6DY$&4ej8W?c zR)tX!FXP<d)3|u4lhmy+Uqp$XBh`=Hl#;VyP>8riFBg~-$jayfYCJtv9G%X=anKy%6mhkPSSVtUt7K%* z9BbYqVkPfT5JHI(T`EzTC|XutL7}ysC8YGddHMJ^q%bUi@#q7*c(f?23V+C^AaSuY z>fe1)+R3V9{&2WzYk~k!=Jj#OfCHr{={a1tns+9wqvD9|jrtnWt;gGA4zH2rMHP4d zS%Q+wm{f|t=^0i|D7mc9&w&xaI2dnE1!R+{nf+B-p zvkaFpxbL4t$(wFbh^|w^LQ{xn(uNh816?p0)nB_d4~L3|>yGe5YiNZ;EK1es(tKWK zg2sIqy`1Wnc=Y3@X7UE}633X^9C@6+hu7`Fz7*;_ELuM-9*ZnLZJD|Mv#{(1%pZ@sZ`Y^-M}v>#ctl~XCiCquJ!kjM(p_ez5(8b=-9_FE zG0>zibu)Rh`7P<`8KVD0=>h7L<-^oGd4}6%UYE+$MqViN1B;%LUW4~VMtcbMp+30+k%_DieM^ zo0(6gv-i^%sVE}zbFW1<2mkyL|L%(_hqwtQeCVVRPz}ZT3U}X3*%!x+w?n@`sH7p? zAi3}Q({Ez+&6;ZI^VDW%^psiu16GW#t~biCl!kYlT9GmUKbB;|uz@~OWvukU-RL&~ zk}>ho6fWSx=~XsVWZP6Zi_gf-`5eFN@HD*%4P1*N89am*phC+zm2kgr1 zfIuA~2rn<>l!WlX+D93e-R$rbe|Z@s1AM0im$@^oZ>HJ;OAwhn}ghE6cixja2gjqN-^$qV{Wj5i;VIBfy&zS$41(CuAVe zyDK;=NpH1$JStzi5;XG-h@Va@e>%e2%@9>^Z1vK9r19OorhRqV_S2>H-p}$|f?C#7 zT4{=bgL-GL44-abwxVnK(gyqvraL3?9Y$w<^`Zh73CR5I4K!jq_pF{)y`7;_C@Z(3 z-dS($Anazqbk+ZQJpJyp-)hb6Wzpr>_tN(1&nvyOLx8-9BzgE51y(ecQe=3sJ^**Nh99)8(iRu~aSKsU`i0yiXmvoHbd(T?fvr@P)PSas-P*5>jvi7Klh2 z_9`@f*0jXA!Y+pf(WX>lV!|Dy#ObvKnJROupydIM5(c9s>1}#Ry1x=sgMtY3V;Ci% zAR|N;J|679f{@aQ&+#%$2NKqG9*Vk)qbXB(KS4?q*Fpw`c^Uh8%JOCr| zL|cz91n+)u*lR^EQ~a|_73I5V8qstTVMzSh`i>{h?6rxoUvD|Ugs1*j05|SL5z>qJyzE+R9)tJxc7b3-M3`P z9vkX=^QliSmAc*=C;CWlOAwG*tc6zZe39R3P4I#0cd(ZDo)6!ZiwV|=PrP46xJsot zLjC>&eSra!l)rr-9G*Lrg;9}$;qFOvw>65iZ zc4q4Tiwu@NwHq8*EQ8<}! z8z4ZAj%^rwW#LXJ4J9Dk0$kU2UVPl|Aq}K^P?D)u z#z1HbG1k=vXqULq%6%N5K7dO{b`BGxj{at55a*U;0sjZjD|C1a06;sn_-x_8jePj& z1q1@nS)n65zfN&3d@@q}EgTKKt0Y9bI3^Q;wA|kOUdul3w8y%?0bVA)8+9!$otxCU z(l#Y7H8f7dF3JG{(~Sn%fe7zi@3-CbHS~(CUr&$~jt@?T*tNuLmeUX>pG_h=NW;U) zC>Eg9L3Kdem8W=e++40Ln>xvOSiL|qywmvdr8c!bn6ZFG*zHi|Y|_i6Wv{?0R<$k* z-uQBA#^LqxEN}KYA@*~b*VW&~J;jyF2+_Jdj9Y$5g67iat1Cv<=U(%K-|~fveZd@! zv_7}p$g3AkOE1IGCQ(wr%J|N7yG=LH;y>BF-&hsE*xrhKn+f#~%Fpop+ekio+J9Qg zD(#%Zz*kkPV?4Sh@X;j}lt5FyqS|OH+)=VRKVsgI;Hgu+fMtEQ0!P_n1en0EUJgyO zPa5BlEHlk6dxH?ov_~rSI{JaYk@>rKa`S&clrulo`ICQXOzS9T$FC!agWgL9WL;2- zkgexXzCu{3&zZUI=Z)S2D*gl!*~vVO_9#9b<;?pyY>CP1?A)D*;9|`O)}&DmySfGt zm}MUg9R>yz;f(h;(m9WPJ#BP4{ifEDEJy0_S9NYTPz;vQqK~lp2k#1JtHtx7Ba{4J zE+!6Po%T>1EvSurD|+h8XI+u1+Ed`E?xqp_<4twFZlfoqP>iziB1}o7q%WJuF6De8 z#DFcOI{_X}9A2Lx%^9(e1ZNXHP#|qFZmu zP9jJ?dz~!|`@s0;nKbo6ZLe5g{uY$`JT?mYoap~r=@vxb)^fc#zVdmxfF?l`+aB&N zi{!DM$_;Diw+^$wP@)kI5Ax>QuEh4eF1?qR${Tp(xm2G+RLx8L(_2V@r89l~ z?)}xJTj?f~eq!8p-Z%bV8~)yugopf9yxxu(3wVCJ*-0Aijrgm74)k`1tT&HmflwQF zZ22|?JZSgtMA^oQ=+n;yB2(8WSXfDPdEO~+hXTk%1eeVHd~S6K6PTC5lucg-!czv# z%femv_cea1KmIc*K4o2~SraB?%a(v)Z!7 zuJ}zAMsxiV`gc{`b*FMM2;S=KLX&=v`ReF3Dob#a&~kH@MP4V9%JYE4EHY6{zWM&u zKwR)dn?}q7rqagh_Kn7@N84{t1M3gJ4?jHLtv9c7xQ^rO%~PfrQ1I=4e7N4c;Ou|7 z!&|{)qza57DloXqiMDyps_)GnKCHy@yj|p( zA)9B(1_ak5hrw#i9ZBg#e4Q`60B+(6;3fcf(bi`1qsH&82%6d3N?NME?l>w*x;&cW zPdo75+?r}C+WIQWQmb&Kr1z-@<*<>fxk!W&u%w|a7u!Mv5@w3786Wa4$s4y%y1V#V zEt$eInT?@2c*6*Gbp#eL*-u8Iq`miI3212VYMgZ|)KwA-(j4B)o1T}Y0uFDc$og3G zw_!}&tlRxIfTx5VM#J0@U{xIk!*Nz5lRkdgbEplCqcvIR+GlLg8fDu3T_Qw9|Djv}Ms%h`ej z^lGnOq0N*PzC6FPUW%c8>bO-A|F9UnnX$Gv2^MCoq!ucp)eb^ev+alK)}lNwgNlioKlnsTf4pE0jZDHD528U-{zr&zywT;j_$0g-QDA8F{~`J zTaR@#N?CO)9aT!XuZwJCYUoO|q1j&A*|3b$= zP_sWgi7a}2HcFK?0)05ac?hOBU+YuBcSF(s#)817srYJ+oPXHk9?`cqjJyTTS|1r zb++J`W#j+=&Ndh{f0^Hl3n9sL=7-nw+_7y7Z3^Znl^==)0$D|8Z55n#FLEDWk13wg zeh)eOs1rqH@Lz1OLKA~W6`6%DmMDtXD)b{sS}rG94Ik&KSC%U*%g(pYUTR|oJ;a_9 z67|cQ`w4o7k_(iu_0NLmO}?;Ky1xMOBG!uAz&sudLWWdR27&{DWF^Ja5Y%|p;dp4H zc!<))$JVX;Mgo9MFK&C$AE|ulT-}&o#u2PZ1xB#(O70G^6}Y)4#0K}CDsM{n=`!F< z59<2i`U7EzO2?<;&8u=tw=ihb1xYbz(eZ0;sxr8OCdvFncU{e3#?p#+Qh|*=2Lh9_ z_!7N$>garWys-VNbcia&^(&hp9%qEJyhadXOl2HEWbH2gW@IhS@`&buw=J!;N}2b2 z-E>|{cb0g*au^0m`IdfM)~ACAil=KHfoxvY+}Z0ZpX*8)k@Hoj)&Lf!-kohUpUW_n z)9t!W%zUEsyAv$ohk`Qz#;~ar`L|{2J?))2`*iQ_X{NvUh+OW!-rJ6T&~Kf3=?Zx= z!CGO51**3Brm&7t2y#BG+vvVXs*2i45njOAT4y#zB3O(aS|?*Jk`b}FwU}JasmBCD z+`d;R*jZ>ZE}&o)Yn6dgsiYT~j+(`C;QR&3tZ&E|i+v*Ajyxc;tZ%`KNU!rWs~`Q# zZuSPL)5PbcHm1bSTIkUf-l-3+e3IRpvGyy}$!h=sI|fk70Sq1Of!NP#jtIlO@%{?F zFKJIs5ks7bsgD|{QpZ0Y9*p${u+VuJlh(d&*ZZRzImo28SIMS(o+R<68Lc7G6dmtK z_ruCYiUjQFlXRr5mq~PFV+A%))W()~WwjLC8+(Vo8EaaaOs?*mn%D7_iu`nDWnkQ+ z=fZWgzOSn2H+7i5w~yEduVBww+#A=s(dpfF)$e5p)?U>){xL64e@ZPA84w(+UNeBi$&xVTiFw8SR{cHzl2@*rizW*IV~ zyT#4|%+JVu-FVa`hKP}HCi5vxh@(azFhU=SiQTD^UsK0p5vSM>VN;R%L#!JqjI3_+ z-c#1^6MpKVJ6d^-YT_mVsZ*?-^ovKbq>jZUE$-iL1{0gjhvPGJy)Ry$Om6>UuF3iR z1dOj;Oz5R_8Ba^BHb=pu7ZFc|0>OQ(VD)N5PKP~|2+Fbc9(Iy!5D>Y**z-2mz*Zaxp2wLv{TIvbNEJZWQ z8$}H0by5D}4=~IEMnHvj44WanEnh8;9vid1gRY;a)rIsxSRk2s{tiRKJ zZsNE9TIq-_mJs*Lh-X#8iQsY}ku@|05pHeVZG`zX+eRHzV4ab_iM&Rg{OP3>5*}jg zul$8Jdr-o6H1JR5M0;w6$82Odg$j!>p^Od zDKIn;v5(Q`N@->KV^glsEway=x-6cJ<&CE3OFZ3(=iw& zC%@0L;J9tSM(a(fIhs5wtNYp_$3;G}vWq*TBN-nG<_+eueFrISVHjVwm7Ep#P=$!^ z%sO6?b%L)7;00g(cK;!1t%?$oR2)~3M2m%}0)|}m>KAiFm$oQ8i zZA*9;D9uYAK~FScN_d@q@m@a)X=@T?)s%fK8P2QYQ=l5?Rq9st!(usM##CSn3fqXO zU`0g5Yi?fkx{4%-gOrPJKx>XO)g}y8m-_S4pr*;l|{= z8y?XB32Fpj-2zyGd&z%+G#uiR-EhmY!kq}qvdn+4AbJW#f5lUaD?JaVrUuav>M4%X z`&>@%Ivyn1$q7vee6lG^-xDOn_Qrqx&abVfSkx%qO!4)ynf2IxML58PTwE~kUs9@Y zo+vu>qJlX3ZWG7oEw?Vj0f|Nz!qK?hTU1BljJ?8l``32)dCePuF|EI#V&1~m+>P#M zXhP<-T@OlP2GM@4C$x}PPe&XaDXJ&mwrqGKu1{i&z#)IUjWW`S9u-aksiPVC(KN%& z=QK2pp=s;|<)t;@aUp_;JIH{zOLwJuCvzSOr6sn$vXd;&*Z6%f*rIDM1eQ{scdaU3 z+miba%gApMn|D40fzVmPNGiO4;oO;huXW@09zJ;ReaqC>RigAhMG>E%`t$cMQmjM4 zvf3fA1rz-v91t;(o9UeNo49)qm=S#tO^HsHO_RSVJzomVi+0O81ej|tmV9~Ulw}>! z-m)JEOfJK9_E~jqWUnj}_CsZx;|hGS!xbYJlT~dFP0N!glE97EJ7-xeL&}CJPh)5& z-?@9e>W?YB_HXVwS2ivvulme_dL@A@*yZN-@ic8ozFR%hp2*diJ`&ji%f?04FJxO% zul>4(w(no521S8|t=3kE^LCk@H*KSMSl;I%nb+s|_TPer!PwWGgSE`_FmZxVzcLf^ zCMC^4&wXbTP@f&f+ZQoaBLS`VbfS3`m+TOxp;!}-8-^u-j9Dm_pW*KI8ZG2sN4t`l zdW8?+<-q)Ho4Z)K#hVknfB8;1^}K@lvj`G;2S*2o6ND(R5K1>_4Ry!Sl8y`lF3DIS z8Wb1~P_g_E*n+K)!uEq}Vx}U7{$;0(jJ&eTd52^I(EEe>zzq=9CNW*!3s91|TRPGw z$!XxO5R5ym3}2A4c%kCwR+M>9JYQ4dbQ3^d3<`2hf}xI%1pe-4j(@_L>D;!WWdet6 zt-X{^CVZn3P?rddlB}ATbF25R9D(uIml3aWRvien+GqiIrCm8wMhk#VtW>Y?KSmq6 z2(P0?W1T!s&PNHkrnYX?b{HucRhR=FQPj((V1klC!^_FuZn}u9{t$zXut#mfZ%3(f zYk9e^;{$hAXC%M}HL>p-q)WB?7$6g;$P@!UAz7{!5mCW1S zR?&yXTiu7DYM;Bw*{k{A%hxxr4_jO1txJ7J<3w2V&-ZuTE^SZq8S~*)-E&D#FYJOH zaE?JZCTkn5S--pi5}yPfnPpP-VhkfK3(y#-9{a&(!v5@7KTIf#52oxHoW2#a)f>}ULft2S#t*&2GGglK0-#g0 zGjf*xg-Ps+v{6+HP$o*Ho#Sf*HU^%$Y8B9QOLOh>SN%beO6OSDcaJ7|0~Cwg$45Wl zyB&KwRtX)7m-4Gm5>r~a!kvZe_mT}i%6+Fl7!pQ6_|Ebkx&yijU*S0E8&uliBcc)m zZVpLAm#Td@nTbDWG&lTSDe&@gw~^s?bZ2twD~Kw>X4jwPCx}VoV|}^PerIiFzZx$$ z21w@$q8Gt7y!L$UuGd7XZMQPy&Qo>bT#n~ywnBHYwps#i8#`D&l%n;=P#V_PVXQtw z56$sWye0XB4?-fYdmi5I>zI#faTEW&dc9r|VYWYpq`$^0ebAp>*AsC2^7xV7D9Q$P zYF0?R8iCd!2nz{5p4X{`uriws$O8hAtBsCrrMOk7JNcjv-YffpJU1uB9y_vjDQB$HMi9Dt_ zyO|u|an~+xG?} zk0$~FjhM0>8!Z(UX2)z-Yrnksd*;Vn2el3@M_DWAlht@=el)6v!%XR};3raLbX9V? z-D#CWuPN`+(N!X{(v#9Qt^3DK`zadJp2?vitGX^T!f}Bxaq<;Vqd8gulaSBQ;A8RL zKwFka6k@Qu=SAoO9)Zc15sss`lfZ=wtG!`u3-tch4GVS-Tpc(vw!iX|%G z^AxGe{n;RWDx71F$R)R?)A^zz)%7Z1%nw2GU>lEh^-0xs(b@5zVcL4b5)GZe=POJ_ zS)^;?Y^1F#)o4hUz%)8|zp`6u17^@*QLi^We)m&{gbU+z_#qHTuSl=g>WPgK0ur(P zbc9ZsRycW>SYZjazNU&c|1ZiEKkfXle^H55#WOcA-P`C&b8}~FV#aW!a#F4FL7-|t zD%ARAE}8Y#X!|^LHiqnDo^K^K+Qr5v*-SV#mJw$2@~fGj^hMO>5SuoX5+#%<4C&h+ zZz|c0tTl$p6dc-2IeDI$X?h@B64T0IHp-{TxKR~-GFkX+??EGMTI67$QQkEBC2iC0b@^R#J>uVLVQDGt)dWy9&Zzq?BvrkoxO_kfX%)+*a}yuJc*$6|AIsO%Z*H-}1TfS*fzm1Z(qq zxm|(I=XMkm+NdA9OalHUOZBh5=`VppE@VW+ls;>Z$9ib}Vx+Uxkr=*-U+AmXeH zj58^CUGYn=`*ngK;iSWk2s%(?iCKU^{PzD?I?Jdk+inYQIyWF89g37lOG}r4ARy9R zBHi7Ml)$D#K)SmdDGBK=>F&;R`+jE(#t?qGV?8V8ye5srTYr`L50IY3?(lXF_}*hO zjcLyESKew@U6*bRAoieBp)2YO`!5PC3BsCUMzXtltb<+EreeG<52j!#pry*ff7sAG~hy8 zjX`ua5?g_2I4^7x8+i?NfL`o-(N=}ZvO3TvZ1w$VlM9#9DjQst{i1zxt%vv+jeRe9 zpSwtI+AenFR>j=^@1PwVt>)zRqS4ucGy_>6dir738w0aek<|Krw8 zpK#{Fjn8mdidAFY0$(sFvT6TJE@)(;DBUIZ02L?8ISkws#B*Lu)Y> zA#n}2*v+dme>feDY57}-C@A;U5NKFFZjpU?CD*(I7iF~)L%yc+xiL1lE=`NGmR^K} zb@Tou;qi+A)<57IL5=R?8}sekvsZK+MRY8s?uZcO_njp_B=BT_m*=xG6SV!fc!u=i zqUZWO>svv3aX&6{bZy68Q{1?dQ9Ty}*mjW0_kYLdlJ|@ZVv`Dizo`k2k%DdmOuwI? z0Y=f2Ueww8TiIQ=6+U8Lr7W|+hK*mdMYHBrL4}HM^s7 z$&)YJdDM<*FReJ3nOu;$1LW!8w)(lO#;WAqtt6^GJ(?wY^!GOIZ#>9a)pq&h@+)?@ zau;=G%s{fZfa`L0AxR!VRW;MgwajgGHst7Fa9(@WT(vbKo>D4wH-V;FYJXSOTm`a# z3dkM;DYZRLip5&|&nIUOQLwIUR-&(-BsCTNmC!IRS}#9(pM@^pR!zhaug65KOj=ie zKcAl~ASEGpIY9CH={dWZXngc=@Z)K?>;a84g!Ee>ArK^)1EuCjI`@+IE@kVc*1g^7 ziDBLSeUwwNZ|XSomzvAT4bc#+TE zuo4lImInNj_=hH!WhpDC-%PJ}s8SAdWZ}$m;0iVVS*F|)^~$94o5tI%HTC(NfvoV+ zh7q5?eqfrC1OM|7e!Q+=8NsWpoxNymlf=s3Yw6FJx7Yl=yuHcJEP9_il-IR#eN++- zF>Or!{k!NZLo^$H$<$TrrjielNQ^26*jyt^q%vXSw_AEJXY~tw1TS606F#ov$(KgkYvPwf zT@hEij_zuJ+%M0Hu6YxC`K)F6`cs{QTB-Nl4Ykl6buA1l1L15h`YJ)u|FXoo;$e;Z z={)6+*Cpw2SzIGm(I&bFJy67Ix)ks2g<>swx;)N&?QQQknQHfSpLAxnDc`Da*H(ng zcI1l)^!UcU`@@u9vX^n?cL7t;?v8WC~+cp;p}?(;SxhV|VR-d`~;up&iP;Ziz1~4Ykd`(?C!NFk*RlnXs*`Op7u zg-0r(4^X2+k! ztE_fjbQ*O2m7oP0jOmN11EM+xvk~$1p$E>?IU#7}#N0txU1qU!-!-D(`j(%U8IG7r z!6Zc);g|STX~}is=(+%&>aU5GzmP_b@e+-31|Wv4@WHNFte?WJ*)XYXfa?(ci(FcV ztx#!Bls`FLHjn~A)hrd3R4ozi8ER~H>OE5d;oP3 zxQhI`JRQsZ2yPIh?TMG9;iai55RHP7Gc zx4jRWo~iAvnI&;Yn$W>WNz(>rBj4WDtgp~u5r>OBx@nQ!+|RO`SY3<=tm+0&D%Z!x zvw?lLeTo#K__h7c=;?*@hhl^kK!|8}f@waU@!wkWj@;Y@^#)VidwcKnKON~#){D$C zsy;n@4Qm&n%iS-XUaZ=yaObOPJrQ}@Ha|?fePr8GRj#}t_b0KCY7{95U+@mY!FoOW z&h7)BDKeQbVgJh>T1msZo_CGDysAjkQP~#PsE4diH#Gx(VlB>f>G?5X6_;?5D*&cT zoDPkI{v$u>CewbCVYJ6f?!AWwz9>R*-=J=~*v~~t+)zW9A{hISos&5F$N93)NiT-h z`IYz(fr@TR>O1El1x_)=NiD?$qq$%2vsPyra!$=?Gj)dKnOWg`Z#`S;4!6X-e4z*f z^(>Zpxd|OKwb2m^-z&ep8nx@*Kle-R1GEl$4O<4qL|rzSPhYFrMY=!AP*NoW26C$F zy+j3f50VEkd~BG$jlD=ff^DAcP*Br6w$n{vdNuL##OJ!@)@<;vHE_ZRs0 z(qm(uJLsvV)vv(Yl!IAlwy?P~qiWE*Wu_(Ezwhv`r{-8W>WhHe$iudWgyox4UC-g0 z9B?cg{8NUQQZ}EKR_E(`{jU*|@9B2;-hQF#gTSfzZ#T)3XnfWDxP+m5(|YFe7^!>P ze+$Q2PkjC1VTER0dG;?HHYaOZ;mW(@nm-iWG!|F~$nY6VM|Zb>rJ92MzH&GXvH=hY z;W>w(qVyIZpoJj!WzZ%e$)OQflfL5GBXdtl%Et_n>}_x;QsX*Y2*E9smC?85Bl+}_78Z4k#ch&c`a|Y$z#YeWADNr`0=AWR$9y}6D+;q@CjAObr`&CqSP&qxh8~o z)=_y_m0QfEIuDL+3QC6P)2}mwOFh?;CgQ$s*$&iL3z*BltPryYjcZFuwU<-){%~hAY8HGltN$!f77F zCHTH&51`3@qd_l9C8wRYOV@ayZnKZqzPUkJ0Sf*&+IayJW@?Fhv@#5n&=IIOxS3t} z8TGx^!<_U9!&qx`Wf9Z-&QLT-`wK9g7S0WBcY%KgCnrQf2Gz1vM!C+zwKAJH$QVa- z^kHoxsMD`&y0TlM$@KtJ-STIUUcvWV z-CzE3#N>noe2XODcEdVcdOZQb+4Y%jkB3zRA1%bZ3QS(SIcU3&je5Eg5+jd!zpuc$ zaugOJ*2;>KBEF&+vl|`S#8I z@s5YZz;$=#&C`MsY;67I^D^v<;q7GY=240kv@!}_y+*FAU26}>eqB(1rs2`sa3x?5 z;nLOmfWM_0ryOb87`Rr^<49eo0<0u-TUD_br!s{yrOPFGrX-=s7#F1MQGX?oy`jdlNPZw=+DntQ$Ud9wraBykea0lkrewzmNh zzx8)Ec<(*$hZC!O>7!^*!>ENoE%2(w*qpXU2?VJl^R)_|FI4wJBmDj9+?=Ez=$1}1 z&dlY79@s}OE;(^>9BvB|qCctsx@$sY-?p28ya?Ev4^Kf;)HtHtJ6Qj4wlwfpN}I4f znK2P#qKHaWq{ogbfMYCPB+z3l$Zsy^4{NlQ!R zp31oxq9TNzFX4!~Davc7CVTI$?_Wihq&Cy%RWc3gkt%KgdG1cz-a*s{vMehKJ%>-G zaty`V&_O-=+AZbJuMnWU8Umq@n=7a}Fo3BG%FGw%PQ9|*F%=N1t32Lktcq%@zV6)V zH1NaK7=6aXPw8FH zfpTH&&_^ZndhfV!wB=_S9ZB(3z!~)d^P9Wn$NV;yDVn&33&Q0NR5Ck%hY@H#9O3!V z-QQq6#;M?X(uLH60iKz|BmDd?A47Pjz6Ni8JL~;ZTjm@8Q9A~gIC!C_0>%0YR8T*j zENB<}*-c#v68>Nw-P>CgY856Q^#j}TIP#IgP14Cy+hLBQYrTtT%~~h_J0-9dhc)n} z@OVn-$nC{^kOq5l;$Oi^0XU-$ywSkKZOb6?R)B|#3J}e^-db43vI)mdsU>6u{LY{3 ziHuMX-7}(rD2L{&<4Re8w1XUK^^$dKJJxFS)M`HfNtU6_1Ki3a3Et>}UGt)6E`cgp z@r*`g=41cNw|>+QnSTX;9Yg67CzXE=A&_cLpwBLdKufXXx_W-S%7GQm`o#>s5B*?e zrZYg0FQTPCPW0_HT~&8CxX@tsvJ3h?b74)9f-t|K9vOMcp&8ZlZ&dFV+RYr>dFzTnS%PM{Q$pp{=rV< zS&I_T^kUxwG?UO16SzwV*yo7Et4k5ahjI(G-~M7!r_5@SBJ#D*x2=*RM&TbDNx(+x zp=cgzI9yr3ii!|)F;6i&eD_g*+Wg24iLd#YfL*7KcC`X$W3v93&7R>qmv@5o&5SuF_>U^lNR+>Crg5Oqw!_(lL4#w zqhY*0NS=U#{X9ZfOEJBLZ6C^%AYwRwN2Vr3{4S7BpbwH{>lRhpzh7994K+#XXu@{( zJmZdC@Bef3c7@7o{IGW3MuY6p-TRtztL@k_xQVKS9thp3n}TY&vdCKR`cz7=+gJJy zqu0Bo>A1&@yEO%x@*eT5y+(L#SOG&-Tjj&P(Y}T4mvlkj)J=wWfb?yNfVD4>{?(b! zw65i)nn!!1>d)tADUD}T7_YNC7sa53S)4NYA|d+q9w)oBNcsDmb;{!l1gh4BrjP?q zI7OLnj0NDp39trG^&EgL7uDW>CR1Q#ip0R((uee*)s8}xa9TYzeB@g?%EKjs+RXppS$l2-ae7%R~-{lEYctK)-)XF)~zBpOQb=5nktrqQUYyA#V48)MS_48kMLpGHW9$FNGajP1$h6T^Q?cq`4XuNY( zl;29#dIhXq-bO!Ah^ygv%1>MzE$c|NP%HvwH+gO%<6p?aswA)9e~}DwjI}Rcr`Knk z$j*MX@=;szqglu}EPmsa)=6PT1a2p{_ODqnPJhe=b7q-mik#Xpj?%Omj5xh2EO>x> zW_5C%w02LqhU}sR&Pjp{wn;Woi05Df69RXnvNUo$#{~l85>@DyVQ0 zrGiozD<-Grh@FJkWZ?{-N{cMFP=Lre=ssln55)%v%s3g6izR4-Bc$=P%8xKRQJLXT zBxb$1WVFvABwJ*QHU;S9o--BGOElts6h?ZJ*IU1*e++&YKFZb)RyP6Tnxp>Rk=H}Z zu&+P3s9ZLGW&FtEj(xPKF;;^EbzDR|`{qAM2?4`dd(t1zuD|4TltPCuZOV0?3MiYm z{oWY#qF`8T{q29r>d0lOA3XOqtm)#|QvPFpICQYC>FaNQ^UaXG6EUd%d@1jbn$#N= zLI%d^&tw=)+lM*?>Ca@O872B}t0Qv<@$(%OiXWQ$?YsFXf96`CzMfyWFBz)B{J^GL zOhk{X!Hw$N>kU)N+67$2Yldy8qB5XV~oJy996YD!&LlSo2#PLl->* zuu&SzA9{7{dqc6kJM`U@SMM)MESEawv8^nT8X-Io7Mh{ON$=I6f9J#JI($V&XQDjY z1`nZDgYO&-+;14#xt6~|;HOdrl6L$bBpaugQg`Kae=CLgYX{aoU(hyPG#L6ktA!|C zB1D~d-d&D=FabTA5-s3+a~QAWovYyQRL!NvHBs(jUS6?NGa;Vhi&=+@m#h8-{1p5Nj=Lcs2ebFC2JF?88^Ae=+D zZhyU*nW6aOOySlZf1|hgooaXhFh&NKJZ45eKK{H*_^9h-XP!km{;{?BNs7X@;xUYF zn1NSHDfvO_3MKd!I4R$O6KLm!7{;D-30Kpyq``6g7;Mmvu2^HiF;}_qL0?Pd&isle zSJO^;Eu%3dI`RVV)KcN;_Ju%J^&?b!-?P!`m73tUGF{Z!YY-Jxc>U=r`1xZoi*20H z?0uJtx7psvp+x9QGg#2eMfhjGXt7OKXBXkI&f^QvV?5-c1=OtmDia>(-M85C)&i&K5L69M>Br`I$-9i*W~JdMvHRG z!4Pi^u(1JVxP=lSi{6b+GPCckTz99sUY7;S59L6m_~juDSW<$5?wV>J+@tDkwAyNB z(KXW<+K=y_ms&5aF6T~qPg6VdZjHYA(W<2--SF8vYmehkssoowgLYX<6UX zEfhtF`wZq&@0(JV*)+)YCfRUHvZd?A9ZO4|Hgo6SeA8#d&axem){Z``LQ6`DzRWMs z(dL#4Zp5s4Ojg*MlEW1ydC0buLnq28o)TJD9_hVc$+wDwh>o0`JX z^%&{8`OWO=&dJgt$W3bAGoj*OW>0iKj5A+$p>_RQm?A+;7C*Bz%9I$D8%iO{gG02> z^rn;#9*bFPneVT3{8y7!oXPD2%bu%YYF}-3HNJ+v!&Nro+z#ZgP&C;^(ZysWh`g!S9C2;q8 zsWgSwb~scuY)Pbn#}%AkM5w5_11>kU&rpi7y_zN*;>b|+tDaoH1HPCDh2=TlT4*#J zA`wDQ3AtD*)0{$lDq>!=Sm0nb00)rRD8Ra>q{*Q=!zL=jq>DMEj>4Oud*GVZN`T$p z+pTsQopxUQYA|nT*f>bn(a)`?a~v(b?Y)ZPN5rbdxt$$*3-%f_e~X@$>fWv6^9Y-s z-c9-V(_T2I=attzAk*_>bLPWgaWTlw32KDmfrA}&eAJn5i0?59)>vgoL?V|59>uc(IAy^1oDR>Dq<}oXO)HSX-elJ8kmKk z(aSu7K^BZD6-;bS;bCGF?zd*b0Oo_gnP6|v2rTQJ23h4W`ZuM8vz_v|#;ocFcRfwb zj}-__Ch1Pumk^#j(6;q{FI;8l0A7}0-#eopZ`B%tFw z>82KQCI?RJqI?k=8@+_3ufw_{7Hdhit`@;hq61{tL-FY4{WN6X)84tzHM^auRuO7{ zk}9>^DAOBn5EwG0l}B)Ko*>(~Txiw27|vfm;9kAq7S@6IVBEMz?n5K6bjqQkT*F!V+5a`d*JN%;uWe()m7ufCvg2MMa)CAcM3j9M|O?=tRb`vnlbD?yge9E zEn}_hVBz!8bF~PIh5hFGL=)Vm!Ees{)y%wedi$;qEj|^~d0gn4!ElH7#ksd;dV(s5 zc#rFHeS`6}&9s7;a(>6JMcol31XL+z4a~U*pkL8i>knS5%8V_&-?x^3JF|}5=0t{m zIr=IduBt+~p@hOx0>=^c*9I@awfM8F$4?La@7cA_2 zUfJleDENgj_#@uBHno_0Sl`|%O!{lT-7cQR3i4h78meL!=-7CZnBUNz`-vBP6)w5y zHM2KT@Z|nfWUjp?QM}8&=_q^E0;oibK#Z2VP;DLRAlESp>L4(bx7Qaf#p^QSND=^`}Myj4F?PH3G3O^_VF z!*6^q&`7TO!(|Pj?6GS)pToiH^sX0>bcXhXHh4bj{~@IWXg{uPWP)&n`EL+f=dV^n zWLq}}xk6}8PER8}-JJ$!wS(78wq=%P&73c&VdrARgQy-mA9n_48&P*u-V9a4!6Q1s z4r({aKdIghr%z=-{m`fa;HtpcA#n?W(@C_8A;3jT_!fQBTyW(42c$j{-!Fn|Ih96) zUbOvIVz2_$(CGUCaK3uO*)N}8^)8}dah)}%+E_NfLYf$YM_CLc*{)1Z(FSxpn7&_9 z_x(NkVoXDuja?qh~&eG_AQf=H|`zZhkXGJ zJ$DlfoEYrKt>!PLjg1d)KJ`V*?5SB8OaSh22GGqqiPaBD%y~pq>S^YbFn*m>!RsRx zV&_RMFcH7dsC7CxAxV_+FEjtCaK)3Lu=~8EZp;qekeSL3j*{>kPiqcz{iPJGwc?u<%C-;hN(>z?0%B?eVce|aJj+zuydx_h!F=i?uL@x%S`^P>9Wv^c-< zGiNT)kQppa49+(T91NZ#T)jd|<7sK|GT-9c8rt~<@nK`d>?C?-V5VyH^3=#v)ac2@ z;VH3RcGMY-v!sbGSNHT$4H0p81!3RUD(-(z<@- z{SQXq+e%MS$2_8_B_?u{|FsMxN$zv&Xvs4#rykfws5{B@Nw&{E-~*wLbx&g- z=djAjZy6x|SMQw4rsMjOdPK$#Di?p!m6JRrwmAkKsaEaHki!5y%9J`Al3ci@&PQ46 z%-o``oVBwBhwu;1D*#l(gKTDeWds{jC)sF=oKtk-myuYD-m-cYkU_FI>svIR&?>ii zzT}cm1g|B|+hg*@AL?^+=t3frQz*E6Du^%wufk!N?bRNN@+OYbB39D3mS#ens%oAa z2D_{H)HWVGtPabHFP^W$x0=l&X&TNi{DEV&W4rn?zV=C2cqdsqfd63At5zOw1!0ai zkDlp9O7g0S_D6aHmnM`9<~B=nGtW0&a2x8K@nw0Q@&FUO^-A{LX^3RUP6p*AiZZ%_ zu76h(+Uo3faYrLa`Dx9kZLNZR5PC1R$PrMmx?Z#N8iySLB?JlKpgmZ`FL#%DE%!@& zD=OIdlz?p2)Gzr^ll9K~zB;xEd)X6EFa#QX1sX*JR`fU!rc$l_kffhB?~OQK+A!s; z?FUB257L@PS{C?A)gDlp-bKR!&KSvV+ui2KjPUO2#BRX=2tTOiJ<8Zr@ok^0KmX(T2@3vk>r@=B2+|p9ZJ6&#sntU}7B+KXW&1d}V%Pu(`W`|A~%c zdMjVQK90BM zGGTj(t-XF->~A?RNk_B{PsUX-YnJdYT>&^;Y!)gru>Ih52O8%6@qX0o4~y+%v?Rxz z7iSrPuCyDHxoG*5YhJeA{O@}TYB)$7Wg6J8DkCZkB9nf%bmaKbF4lGh*9$qH>5DP> z$}->J)OLKfI35%bnQq0I@#zPe8?tSEa72H#RtZ>O=q~jP%`e@~S56vv_! zfmYg`;_7{{JXhpY4q!9!7Zlt^2@fqbN0Mgwozul9gPP-mnQi!4-sqbwnhO5!)^H+- z;R=z;iuw;dt`SE9W(lmAPzX*ID$$!ualftyWp0=S&~k+oaw=n1h#t0MZg#=3J>T^E zhr=qP`;FQh#>OPv$&edc%;VObkDt8<@!Og62D?#Pl+;Lx;FG@yLj%g z0X1^Q+>)^fMX?6J{B{>=&f|y+vhUZwUE={YQC)lr*V8puCxUFtLi^?r`OLtJo9+l> zR0vKNq^leiZ4_s3jjapjgJAY^Tvlq8=vsjaAlm;#w}iLv#l-N~(>UrY^#j~NHvS87 zWdZIQRsfCj;8D;~vQ0X`T@+gK-pVYT4iXb-mAP%~-bXesf%*{BI_YVJ2YnmpAFOGJ zQ)PZ0A2WfAt`ijmfqtb-ax-bGHcdOz9?P|GlQUjEpML4X|KeNRi zi^kbax(EDdJfP;HdN@y&l*3&9dQyJWPCq>5!q3Vi7Zct2PzdjUw3e}A3)k%D#J;nV z38L3rgYAvLLxlQj*^D42#rvc<_U_Q_KpXRqt3U|fFCJDohTuy$3(h+UO_XxEN3>0f zStJhldDypHT|~yj;>>)2F!?oW-cW!frlR$NAmM=I?c&lTSL9yD4x4+E);qa0C!S5A zI=Wi8aP_ur%6(|$7ff|Fpi+4b0>YNn6Ab}lArQW}FRW#>bsh_I!I7iu3<&Z!&;c(H z_y!1gNNB`mvf~`A#E>Z=L_N!tU2n!gW-Th4l>jHCdZv+Qm}pMTg=n90`{berEYlN> zt76ZTUW&beL#b}vquV3wY6?O=6dbf6Gk~xn)9t@DEhlQCZ=Q2G03V?r071F61|Wl= zKD>07oN8vbpkYhe-C)Wkr3!!U zzmJstUB)3aY;j(G8GfHl$6!$hVxWMm&B^%x2p{)3Hbd^P*%u4%*hcD^8 zJIA?r)*kIYNqJIri}`Q$^T!3H4@y4A8aC3FqE}b&WSwoX-}UJQ5(_&@kD^P6sVuTa$KBWmfBn7WV&aUrz(-Cd8>-IP@aFWdp39Yj z%QOxlI_l`Ze<`uxk;SGPGJc%h0WE-h1zq;eV_Enc`u6Sf9Mr#d6o^}GG6YE8-bL=8HDWik^O#oFup8PdJcyZIQA{`c zHh6BCj+gvBES$}*k^BBVbHjCt_PGFOxLx8_RxnvkP_Ze9Ok*^N*GFuHEi9czn@_^c zr8A*G6}yoA%!d5Ms&GbOE0qHjTnP556FGF7ZV67BrcSM`s2ztECTVUVi8s{eGc-Au70`p6b=L!=9g>udi_4VD`n$;H6ED7My5yNlG@K=8%wJVk zasYDaVut%RvMG~h#|xW`f#2XYcWOJNh`ms)ya`PB!`ra-i}VnXKZmyn_mndm-94uH zdG^eLQ7!+}_Tr)us~Zx2l7Y%`8wHx)n#hKXecio{j^L|-^kuwlCH~y7fzvibGdiaIQRv_0Rb2qz5^;U4FIGPM}I-2P?Sl*WrDEo z5&m-dHBE@u3HWr3TUgY`(V@>ISK_+V!PS?WKGTfrM~mQ(R=%GbIlo0jpN-Mb@Hemm z=yZ6WVGta0g8Y#oiW2~)5-}>J>3v0>w>-GTe6t zJ>#k98(b1{;hC0olKkMVnc+*b>l;3y-Z$gky_p~w#8wa^Og3~Pr(nic+tQuW0iM%T zeccH8#Ye^#?RPg{ypE_t?WYx-Q>T%F+`A0@D?HamJq<@&42%1YEU*P8R-R* z$@4o%%7c_HCRp|R`*&{_;GbmIL0TTEqR2-ECru3RA53<(T2{IYK)_lx- zQFN0;!x;-q6PU>1Yy*1fhAxpH;-y!VUjdjF9WpFYbsvvoy*W%{%7rMx=vSER3+`*Ug2vDp9C z%3@F_aLXJUOmnnGa#Ebkh??oc0Q-w3<%<5_2HFisBW~^un!5ce?xv@A z*KVyeJd341!a&LljRZKcBvwFtcxhS3xX;^5!rLdmeaDkQV|WI|RPUD`28oOVp&!&= zX2Pbda7yU+DhoH!L0m||2L)1)UmO3ty)^k7XD5mZmUffVcLY%j;qc{mfA_+OE0t89 zYw@S$8pW;%Axo}2AlSYjDU9{PZnv?BE1(1Zh8NG7iP_?_F}Zr4y#P}qL5`gPDiz=d zT(8ZTejZX zo|4+hTV9*#A;`*aKi;SFP~g8jkiW6joTfE7tOk9(D8)wm1N&2FQ9|pz=Y-d^&$V0= zfFsf*0!bAi(e^Ae`Vx5}ng%Hm%1Auyd4ENtlwNNUf=eT^PLj)j7?v->|8Mt8u&MEO zg=7K#oi@y+ZQ3}WsTAGmb8be7R4(jg_`HKM23H7yRbb@MXQH8Z!?#H7FtZnvP=KCh zJ4*6U;{k8?O~-a^Q6{DX_i3NW)8 z-buZ2;82@?u>?ZiNDO7*(I28x&cogRcWBf4#2-z}T_y%HGULT`D#PgGf8F$F` z@O(Oe3P~(990G+ZWR&o^)+}eha6~!%w;PlD83EzlG!Bg)#2%^-vikTciK7*KcBQv{ z3&gOI&ya$?0C?mxS3TkszYsY}V%F-Ut+C>oKvQ}*J=NXS?6U+PSl_E*?LFS+>4Psj zh>Pv(0#@%=G7btZ0&{+ad0*~uX={Zbx<~DU+e+Kfc5C0S?Mr@7_-!?ud}(wVL{=xq z?Rs5j&bR0M%2iT{j5ERtRS-OuCC|LVfcB=15VQce&ldCej$oz^HcXA~-qyu&KRRXQ57{qQ4pNu5GLcIMS*GG_mk*@TBaC{;iLh$99+ApDE{$ym=520D z8F%FiGV^a5HZ}0Z+h~fHr{9^B1a$WxL9B+#5;A^DKegC1O%;5?~NDG8W zV^4$oO@WSRL6voGx%o@GQFBEmnP;MnS?gaCo=2rIGt*33i1e>xHlRNOOh1^UPQ=^+ zhm|P1K9OG57st{cfcswH?(#B)4P|dHs>*670pE-7pKayn3yl3dE^NCRC(=mCqnl69lB-K6ESQy__h|_ z`Lq}=;m@xrK7sdjq&EKzN6pH+nnD_z|A5Nn9J@k~b4|Lp$Ak{*wgxMLH`ig@ZD$er z49hmNvv;2sSf60htTNHn;T1=6cjga|OAdbwe6}6)#V3roCBy#%0GDx_nes6WsC4F*W6ou=sc+3ga$Yde zvCkF+cHVzuX81h&oWMuJ=npF;mXat@`kLrtOz*3bRDL*Jz5Kd2T%m7vZIfN9Yr{Lt z=)2bwz;_As&0y{tJ}S$$xBvKq%yV6yo3F7+!N2tPrx~_+bV|Unk zfeivzz4V=@o)LD#FG@g33O1VrfAYPqG=9oYj~^Sjs62e&BPFg%gkINY{2*{yA*xaj zF*fEGCIWv(A*#2P_ZP(|iH2Aa#T3f2PeRL2Af@ilH4d!kkxI%RM;5bij8G75NjXgg zJEoAO3#fnsf6Gr$5Es7_@OxIe;FwnoT}rdFq)=Zp0_yHRF}unOIwPdThg_6}#;aNHd$+jsBDpPz?GI;ajYELJ3bGa2j{l z;}rX{?Lkp;rHRm8 zASCiOe>ojfc5*5pwmJS~OEC__;LIJ~~iI6qZg%p@hd#kOuZNbs%< zGKI{inwHaCeB|RGJ7|v=;NQUK2|1#?N+q-&f#-gynDw;%xMK68iyvSQ)w&WM?d-af zVh;mkr3krsMl--{&ujPQZ49gmT3fPg6~SwL;Id4nPe4}LYMwr1BTAW>TFZa6HJ=wdf125)|Z#! z|6#H}l`KX&&c5Dr7rVmJ$IK0ivAnH5d*d8CMb~GU^KfeVc5*J!7)*=s{vggbRA0d# zKOvTpj%8Tj1h)Vt`-Z`-(9OwCWN2OehYxQ&29g1a6G;4f#X6(uI6-q8P2rmUVLMz3V@x$~^NJ%rmviUU zXLtiSf=Ox;Ygs~=?)~r~_Y&X%I&x6&~Qg<}~xu)A8rv z_6{bYtJ>SP+YVWItn9VB-kU3INt&s+!DC#X25 z7(g>dzSY|>I^4OU_C<+2+TsB0;AAF;t`Dl+%RoDc)_d?B~eUm6~MK5 zA147%eGIsuU^sq(yWZ6?G6U(dEeR=D$xTToC$i@Eg&Ldv@UyT0XY6G_Up>rSudX$HnK}HMObr=} zuST^`p6Pv&d`Iq#$%-C--oCZ21Tr@Jwi*yVyF3;BxKG9-_S@fAIz3hPcq(=CLhntR zJb=Syx4H7$=c>fC9PrAQHFy^rtfqHhx~QN`lQwOfW0VG<17kAD`E$sva6U@tFF0xI;NHPajJ$E%OJy5Se2D;=G{pFM-2yqB1I{ z39BXe_J(KA^V@e=UvYYKes(2If_Mx4)|N`OsI7pQFZ3F|%k$CKwFX14g=c{&hH@^ZG263rve2Dcx zl;6Mb2(WEf4ZbY*8lA{s=Pjb2#IU>FweT`g>|eL%kTolWFk_Y0TC#2L_^1?T5iQAZXN z8E3K}aSI%YfLOrcXf$06kHTohD#>$moxJUKoJJ_uO13!B^ERN1@%XQ>UPiC}Y7DpO zyFR`7idUDd8!tc!0fzRIriRx^6u4OArRlSQ4n>_`qb%vNr{(V$W-jw@eSzG&JQYw|vJA0sk*;vDQ|# zej5B2AjNgCW_IVO^?M`F=hXpN?{^UpLq7y!A2MQS;E*8@WDc@v_r1rFZ3rWJ2;6v* z5HunBBDBQlw5Uu+hu2YB{@)TAKLWS%B%FvEq?{&p7$*emntzg!#r~X@vEF_Kmr#me zNvfB#-6^KChojOeaivi*oGFHz5WE3 zv4^5D4f|#?)srw~RmN;*QWl|ZY5k>*p4jtIf6VXds)5iy4Y}3(GK=h6@!ufN75r;H z^_n=+7bI$er1Wx|JsD2_p&_JA=BA|yCPjqcI|`Z5(me@v#@`x>Q(=|NMcDx)Fy42V zq{nE{IN;r(@pH}*G?qi!t-ajr=}jcy<)W$tbR5?~l@L?d`SZ73-#?2f{S!`)e(zrN z$X37DN5r^9(eom=gOB!7`5~?GrmMr|Mf^HdP8jC|M&ce~#_% z$`4Xz^|xb+328W4b}BWV^JL(^0Jpn;L+@afc|cl^zy>lodAotRX8N$68B*0r<=+_u zxt8RTQfoTFbM+Q_?)l5c({#Ij?o2B%r1e`1HhmuLz{0|@s|u69l6=GOaWu;0{k>Dk zZtG(Bg0}RC{M<9Y;W6B>Q3TIe9GesP+|rnOeR!v&m4pla;U3+Hb!BvFkGcRD@8{Jr zpppSu`iD{qN9UqTr!GB4)n>boy1J^(mK-IeR1NdHY|K+Xc7G{w8W1US(-%big!nw; z5yUk@cxghE&**64i4F(*LP2;8#>q!t9>P=Ajnsq-lF-Z@5CpyvM#Iw;NIyXk-5{yR zcCgQ9byZM89%yB?K85pvU$>9w-wqhTpEw=Q;P1+NT^A2}o-s_n3@^hTH(-S65D^yvn)85lN(7eYw}asDcVfE6wuVXeN*_H~qby z6s%qT*ccs!uuycoGk2OqMkKNrW~mG0m|yB3UDO zDGGJXr_tVH#*<-K3n*B*Y49i@7tRiDIbP_!zP{tug**@oTP$Mj>bZ4pYPz(8sUM8~ zv6V+dpfB${$JQytM4?RsYSOmLbjLsvAUivl;;|`wzZ7#eODloE(SSB@+KpVkGbG6% z`hJK2rX0DOeOMsKsuqF>j$DDJL~1=IEUK+LMQDVgZ^)Xy5@x;ibmg-*R$+Nxy}BNY zo0VM&q$1rLuPD)OQQSOQp0&?BQa4p(#VayEG~Zj=yP3M8_az33zjHzKT$p+0D23|h!JdCDy1ft5bpqATKo%-{uwB!s6zkQGSEi)jUwdYv$Z~YY5Za(?o5&Vqr)mmsQ{;zze}=D=ZmXKoh8rsZc4U5njebSD%A29*?fk5 z?dk3Y$I~&3j>AZ9mULwoV@SpEln`=GVD~TZ@&jG%W;;K{*@ZY?nLp~qf|KseOhTG2 zZ>{yH(L^EU)yqRvHy zK1xMxgXNz6e>8n{R8;TxH6v_s}7Y(kT^|cfl9~k) znz-rM3lFBJTr0JXGQQ_I%oAjxwp^eU)I3mz2Yy*@jw+JSQweM?U&$-iu2aqZiVtYz z`RdzEi;ce|Il#rceI|^sU%=SFLWwQTWn_J7?F(aIR^%Mx{2NuhT0~7SMzZC`dGB9>L2kSKT$YM!&ZJJ0M&a@x3g?}5466k#QPxXKuE5FW2 z(~D6M_O15PbcsgXB+3&Djf4c91ic1_4$E5rSjUs3{~L;``{dYOrq9+^%9KA%EzLg& z0pv?w2?Bd%&uE!}osSXf<|hb_Y3vW**!G8PZ zm&8lJM10z|T~u7y?RjkglXUS=YV=New9skNMCd6=;~|0Lb=2iW7KFiLqq$D-o2fwq zGB0&1yc;SC+{@M7hNw<|<98^tjMn#Rx_6rmZuV3oqE3$R`G4%PxHuoYuGKi~A%0yg zV7D01E3ci9CuLNO0uV2%2&&r} z#Ud|Y6bw)_9*^QxXwYSz&s_9g&1n5+0> zV;<1R>eLHG{R0d6^;xpmkSRs-VDe{c>Z*58se=5$aH3oHs!}ZEr;> z%z0vPJ~DGK0B43g8!1PeL%nz>Ddcfh?7<@^qmUFKvX0TKH5nXIp?fYEFH9?2R5jY!ej^yi_sZtaQoJfV=eLSsgG9i=%bUqS9?j*lsjbaD z8&CGdBEK$s=v#KW$qR^m%TG{t+u%?3^+`{+E?^)VRV7N(42j%FfAe!+#?TN*dOyF^ zOD&C4|CR;rf0u)}lQI)M=LJ#8 zjO;?rRuN81UcQ!Bal$Le(%M39v_s#0`d4qyd+rYb2Z<9}0VSTlBWyH)7T7Le!Ig18 z-LVWGI$bW&ifw)GbK|t;yhH3&ua_C{pTn~aiRho(*XP&LdD4A1348kQ4}aVKj}=o} znWhD8hS_irzhk$@{KTSlpTrR0WOLn7&kz{1JV-t~#<5pG3{b3{0}s&4oVqms!_#mxO? zRAjM2Q)<_2R5#vjwj912^!j@+I=@Z}yu@rv2#8pBaf(Qk)7gmu7>bVj&l*?+ zmE8a0VD@{9f`(M4-$OyZ%Dar_2fXoyzHh`@*vj*O-N(RjM|ynkaVnCd^Xm_9ha^{c zg0!E>SemVu<)YuDE=FFNDoponllW5MQjvMKxZS~5g+)F|w2f_QR zQ=cB9)t6LPF#^WsUPb;Iu1J=jRSQ$y|G>;T&e%<|k_yK}A%iz_TK-g!J0&2^>UX*P zyywHdPI-CGO}9Nu+cD?N$D4eGZ9Ne3o2_SlS_WBhx#%b2lndhSR3?Qvfh(nQ9gqk5 zCZ!%;P>I2g z%$=Y|+~VjLK1i2rboPLQ2>N5XvWavi(IAAxpS4P(NCM~U6w%kDub)%_Kdm9++FvF` zWGXJIWOhFjz4?nJ!b$)|?LsGQ32u27n!`&>)LpGvKwrOZ^L2B*5s%8Q6)olF?4ZSK z0P!yzUM5qt;j~3Wxd~2d7V7{8Mew`Ib}5SF2Ze(FE79@=8$1|nJANFm1=i4;$o z46MR0c*U)NWvja@;MoOUMgaGxJ=644Nkp0uumf2&>F@gEl&{0(Q=k6~-7H?yJ8j)a zN)*zx3=s31)QSQp%L>h8{h15ZHzuD`>8(3KBqzAhPb$0t#(|McnkV1h;cAk$*dBb7 zSr5oXg}G4y2$#j1ugUr$Fcy;u00%k9WfG$cj%L|RxY4b0g;*wO@LH`6e3hdZ+Qs@g&YksMLou_ZBF||fJT8;Y z3-GHMWNfy=pb_A$Hb)x*Xi=D*D1JcNPp~oN)Gd zItItxgUD3Y7hfym!%#cf{$RkmCg6Y&dSoXmA1F%L;@Gnd2Zv5Nz?(^&nfDd1lMP-C z+sq(`TS(rv88P4v1&)RJRGpm|4jb?Ld3Q1_n!t))XQ#xcJFR>QEkDj9i`uQ$nI<7` zsQ*sS9XSuuqhp+!jF5;7P51&B0uz-})2Le1%GCi;ap&>q1s4v8V9Lj#iZPN@M*9;e z>4!G>SBS*(@(fv$%MCvQT*GJw$T!!vI({d#R1Ugh`O>#K;j)C&OHbCeKvJ7$CF)gtcI~u=A8`@@YOjsegeZ`c=8% z<~Dv3HDkg#{y#A^)fDqq%e7P*ZxOc>f9SD}8S7@%THX)neyz$r~p}tZR%rXPcK9qM6t!FZc&@>nV$c z(p}O{T4?0vf*i(YWh?I9t9)r=QcwtgLhMWFY&yeQ37%g}d^Y%J67UNp{_W5v*)M0#W!nCt`Gb4g#C7l5(XojnqXeHRbjaLSm9sSQOUhM?!koW<=O^a(l4LPms=Y^M?z;GQlh4MV>^uR!cV3Ua zZHL}vJxW2r!PC_5lv{Z&dlceC0D90W0Q$$x!_OZ=T4rQ|*?O&&CQbd!7m@lk&uP}J}<_p*a6i(;U=NJe(fw)*JL?zsy2!C;kPQr z7s8D?Bila}!AV3WWp%G<wB-Cq#0wl)$qP3W)FTKTW&rPC=sM6 z2y0H9u}hSlQ3k@St}n9 zPkn{63XK&zDbFV*wP@;%6_U1&d zC1HKNn}Y#hW?Ukazh7EkQZtCK+^}s8B);RwGAavV{HyLtVtKo~2lm|`P`^+(2yVDh6VLR{Nz(418x;mgbsH8`# z2Z*;?@&Oxjm^rd*?mmamQ_)Lo$o4@D1k4F~=PqXNd+OGHeIxcTa?|I!D&>G!gFtu3 zyuXF)j@r1i!y%6?w{VzS=6wvrc_W3@Vwt>QrU|yRc6X)^51kD>`jKP%xDB#Onlq!* zEZax_Zp*b&S939`wo{kSJl|kkBsqawz&+)hs*b&*KW}!U6XqZY9f94RoV3IJ=cW7>*%*~R%Z3q0b{my9HfJYbiT_^TWJ071X4ci2o$`iN z2tvybcIpmhoOr71Q3wyy+Xxxu24}70`QVGDZ{=Uczca9t)takLt!JLPSXWxz>n$B>!m@G2CiG}X!}q90VN757v*J%L_^#WU z$6X!O`-i?mC1d%IXbq4KPpZg(hJ-ZPo#`?vo{sM(_D029f=Kl8kU=fbB;q#u}{GTpPz^sBlr)Nwk~9 z6)CNpxtp}REuQ~ve@(h1K!)r0mOZ&ox7Ykj%CY^JnTUT#=Bob~f#k3QtYXAE$xD;~ z$8hnbBR1PlTWWoEPESJ90C=?6ki*-6vlo%!te4Nqj*^$OGMW6vRa@z9*kUGdqapmd zOTo#_f*No4N@tZ)yxP~(J=#e|&q?)*lGkavW&UNqUEyZeB=JW<|az*}6{@1ZnOk>HBpuYW8p3l!%+AFav0- ze%PY?Bicnb_4gXKs6EO+t(+k``aE>}TfvWDqON<=8(6tf=RubGvg6t$i+N|CT!|x7My(L6l*8+ zR!{5%1zQTYy4q6(OqCrj9T>LO{{dIE9Wzu`;ES65T~=~@_vG^Oh(gbF)84x!8?nWD zSMu)5+*KRDiEPQjR?>eooPmo~F#Ov=X9_pr)vLZZe1S(U6D>>rZt;G;!rUy#joAJ3 zT~8|b6k_eA7mBXud0!#?p^~Xt;^A-;Bo-%V?}v_wsUzkNIm9^4HMDu=RADsDx@-(C zJgj4{G!9qm{9-M({NM&T??4RzZcUVSJ_f0C!_5(Fm$d_qJ?HxvI$pf@#-;-f2i7_8 zC7XlqQ=2|twz#^PK?d!IH)}L1y?4~Gct02UOXAX2OkO-L_Gc~JjV|p=eGxF8ofsP~&4BRJBIGu@ z&S`w_9=}O8nRw(N;rbyeqShc9{ov2l7_GOMoJkI?Ecv0fg->`QmhYS^`Egr=olC<_ zqc8zv{+sAv>JjV875Dk=AQdTRH5cJCa;jsKW^!bT z=W=Q51g+4`FwV$QuOPce`Xc|l(S7kEio7;;#g7&){;fBUk2M-id%_syvXC>>N@I`6 z=h59P4mUj#)Sia@gUf_cmE4`N>VI4UCPJQTnxG7k@{Pooid?+od>mAE%366yY6=_9 zy0C)p?kFQP5X=+2zpV1>dPn=Bg@_n$Pr$7zEv>7KmzbZbIK=nPZTHSkn6KMO?N+@a z$G6^bH3eLrjQYBnQDq>d1h-#+d}*-lKINbv2-OrJ3;mQ!N3l2fBEKgF`z486@NP|j zC7PcQFBV6MfkP#;Y$dFlfccO+vXQ5sM*7vJO&>)3`Su61FVuOq%ix|v-s zt%X{1;>10y2$!;nb@lDE#H<~GTwJ7DgI5~6tRM~6B=v|=?CEI?k)q61iM{htxNrlI zpJ!I)&)FH>RR-#@7wC`suV1`myIWV1jK;R!jOYv<0{CPw6H1mJ$_t{ zinu-mDOg2l7bB5`x)5b!d{QKPWSal7$xmohI$){=PU9jV`YyH@+ZP#m{g|&0T>!zC zl|?L`rC4c<`2Yb(pX$61l-X3G)cfxUqxB*QekIo)->h`zxig0_akUI!tS=J1)_=TM zzQ&dZ;=25t!KTZG6IEJ3^I$#|oW9`8$fENuxf!}!zJp3%O~96@(r^#*N1r;yFE43~ z+cTe16Ps7V2A{BZvJoyxSkA<+Vhsh)d}&2gLpkOf0incyp`J{M5mho4UDNU(%2o67 zQ6}ngoif+c7b>Rq1)!fAW;QC4{4Pm#7O88WJJ3EV?%!e*7;rQK8h6=BO$9eW@Sn&&;m#X=R93)F0-iq8!L>C4z-FN~=%QcViO-9yn+# z-3Q*1KfnMeY=3cw3U0DW+5Rx2%-Y;F4V=M|mzmFKi0-{0X^2#zQEkd`Ge-Hz*2-vZ zhNvZ1t5mIOm7_x)e7YC7c)>e@Wm7*XF{%9-U*DWf^pWv`tS$jrWv>Uve~;Db9FYPf z!w$zQA$3by~Um=X0?wJfo45Q?<`l7MXEV|<*azRA-*!gh;B~U_I1`BO)weL zw0I6WlTy88JHt-*ff0eQGVsT>5-f+95{vD%Jmm^%eI5yLg*Cl|50{h<;05rOB4l{4 zwWVq4?o8rvb?YW@1W5l8sf3>aU{}<j1%r$w8y?Lbi? z7fm45ZpGZf{DK!${W3Syw&tZ%u;fE}-vCN-6_$rwq|TRdQD2qF?1C+YbkocnE?UD_ z5?(n5BU&_rvZM$gJ1o{+Iw|Xwfx&Y_Z%97jzF59YIgI)qu9&`~2>76nU`l^8&QJTs zyfRcP1HCiL1>ZjcRm>RTf~tR!RMDsv&e(pn2)Q@wR|(7{P44gjY-Rp^)QK3=AM4%` z_8%%r=(yuYY>s5X4oG!wvhEZ*+?gRLPKgHnF|9XMh%2*K|2sJP#tPWH+2y-yjrI$R zU@}FgK?m(I*E6nE!gTxrXWRn&_W_y9OF?_VJxiRQ$$ZwclyX;A78`EUeEVNSQWF0{ zReTO0mFAZD0qQ*A1;}0DrAX*m$CreDB#ZlK#_~lLu>r_`7Iq~ucN<3~h6K&u_7lmq zH)8a-HuRNn+?c+9Cq=K)^b(zRLM!@4C7Z;NSE7kGHf~Iqb`*ugSpIhop%jKty@SAB zq(j#8z7J&&!54YS)><5XDw^L$%t;hHgWptEG0*6BJ|IWhJr2h_o*}L)M?_ryu>m@I zccqAlh>i!U59y9d;=9x}xUP<|5}!-vY9J5&S&00s^m5tabp88_wfiwpR!cqL_GC8l zf#$BR<7NmbJ1*Z%EL|(Noh|C$zbn3dwheb3?(hc(8}1kbv*THp9o8)L_z1b8|axVOUQ1Z+`7Dig59sS{oqUG-m>c%gK8*5zpoO;5{)#$)d`&W=8Xf!P&YuS!AM za{_4|KYAq<#peyy!F&eWFu;t7Krlg)9&|69pc?j8#MbZpE$Hv*;xR@V2Gz2a*!*BN zi?H}$&glBr*v8)FE}F1?Cm;?1QEkorB+2&z=QI9r@S+)_FRKH1pTK^jK zg`;o5AiH~zqa7d-YWaVd6}#R{LX1MdU>!+JB4W#HtGzMsNQ4H0A9CM-Qsl$J(IsK0k&)|d zBdt_``QyeXY8-ql-5S|XjB+fmw-2Dht2VCuZYwfSfjKfHu4L@Ge2pwE?~P`4c8mM~ zFzc<=P_KqAd4eT2#qs;E@B9s%a?~|b=?d^Z5&XexvlATA!B5`d5%uJ{9ZqN}h8DN7Y(M7nNF$@!vH(;*Gvt++d>d^tNih}ohxDC_YzvCBFim5l~-5i&^l@f0P1 zY1D!Y^E$EimRD`8y|1BZv4xuklwOpa^e7oA5hlf$V3O zPi2~d*hT90UY%-=qeHpJ?ZoseT+>u@>L~=P*aK;s?&Ij%eZNwT51B;%So`Ck{vFVO z2wgxG^dGi$0IXfw#E(lfz&kKL@5j}KWqCHahCPH72r zEZ)D})onMw1U2ez)3j_S#~|s%IL}BGJ=!f=I|=*f{&g`27>8$~P|ui6N;6z)rV<*@ zK-I>4HUglLT6k_EUA+#y!jW(DHRr;LUrOQsoRjDM^?VRHuMp_N6#+=2&2jD&>DH&q z%ODDPDs#8q(MZdtp$i|vLMPVme0cT2}RZ1!_fw0)XguT-^zF>Q>N1A5Dy zV_$Q}8j^~~7eBRZAjFOS>RlhX9U>4i(c0kfFosc@}~vtv<^~ z!1-69h{u~W;J~^1r5H!<+^;zwyL&l(j6zzg(_i5!4Kpz1Z7Sjr;bO_Y0agjYQ9# z)SuctP?pA!AtAl#O}Ou#)V=Tiu2O0A{hNrtqQP0C3+dK&3Mg@p7~BZf)d4ETxvHa; zkT$1Ik&e92rJ!?SPi!Blgy8mObr-#xQ_< z4mnAJIBukrJ`XY5SSW2kB#HfXdCa2tYw%a_F#>Whs38W+Kn#i5E_*mv$5dZ+NA3BZ zGbZYLvOGNLXrGfqHbcek3Ok(mU{rpEkejE)>%}F+9i-_xtz*mwA>CYcGq5LiPu`)a zStPcRS!wjKi1pp@`dKqvRy2vm{Niom!Ce&ET_mkNyVbUy>mw+OXn?UGMVRdZeF&(AT*l zgD$kc>_kKIkoxlL-u$w#Z5gtjvvR{1ZS8>eg|3X~ZB(rW+K-*u9$7f6?ik|c03A|E=^NCsYof5mYEjfz^5Ii`FI z6Dj{;E)0U){doFPid$|Squ=Z&B}o@LT3D66UYaA87&n)g*^w#j@3_uV$w^x0xc0z2 zO>Ol|mKw;(@QbLdhm8@t4j&+P-RzTBI%VxVplMI&dh9F=>HFGTzZ^4^H}V0gU8Hot zSx+B-ta)CZA}*)OPLnTKIL08GoUnf&;D((%j`E*A-tdzNT6veXUkp!)T_((EWT}B0 zfE1kY(sI0o!)Ie=*e}rE6RV~{)G*{0gew-7%XDj?x~w_K@(im>e3qqmk-u1@;9t43yNWn`skD-5>(mXImE{8 zum`AZsc^=@l>+B#TIZmNBys!6q4%~Pa ztO49C<-j?ge+pTRuh`3Cofdv5o_L*yg71GIZXoXoHH{v&by;R<)#@PO1$(MEwQ<~#?mtquAoo`Vms?10)K1l^`6~y@JXzR3A zKP<39uAC&p`egCzfdX~*c5MxUsFJr?!1t8F#6CmAblcBgG_j3=qck%t;%?4oWy8%?DVV_;Uck#9?g zi-$=C$Vs98UA`E6{kG$FqT>O7sS-l{F-Xh(;i9`{ju_~_?v9AGxs=Vm`Nj3^R_Ld< z_!7bv#}=R?$NXZ;zFUFE4WgE-Uc(gptLii6*N^f*yd3P$(n4%w?|QqKqFf7dLXsUA z{*=AM9Yr_ca>z{C6MK9(OyO}qF3nQ^_nh&=cFw@9TlzQ?y{okH-x2IC51JYwSK2QY zG0@>fLG!h>S~v12{8JrajKCrZ&Z7J|`Rg>$ND~V^NrIxcuU~N&Zl1vxj`E7uoA)U5 zK9|I6c37h^@=AAlEiR|$qxh?!NMQbZZK+!D;uVF%f$F40s7Olj;BbEWzDbb;H-{=i zk;E&T*GA#$D1Dl_;#^l(6nLB`p5+*6-J3)|u)qI-_ z#j%<$O0p=9l{})lu(vMwPPYA7{MDxXLN7Y>GavYn8kK3dY{153{~PP)@0c=?l!2a^ zYwaEX_!eu;)j2A#`l398&FRZ`hAp)imPRj%B)YXm)!car4-BVS`*dB-@2%+W4ToK0 zyz$o@laG;Trg_4OsbCrkGEAR~x)gKJw|t7gn&n_kkviDi(1$kNGJQe*!!4>(BByrC zQ4Ij5a1AI5mBjrIy&3Rmzolyb@iA>r^g2L=oppLWPg-h!{G6=&*=63ZI)Z<^pR~QT zWEf1qY2H$4#YG_hEah{bJ#iv~qI< zDSKHHY>cFa12}Pi;KrY(zCY9E@_Vr!Dikb<$&kDy2>oQ%0X)KWisuvwbCl&R#3kDJ z1D3sA=jMe>2BdF?Vri$$Hl>H=bQRe~2u#1A=JV`;G)XL}o%m^xj4jD}#<}`X#3?xN zX@X80t@}~sx!#OQV80DPlO&1ew=lwH7&{xY7pA(QP$jYr3hI}+*TVDCLaU&<=Y1zv zik$t-m~DdEtWqZjy^xCcGfXU@sLNtq{y(G@i#Imitm`hY(nsuvX(epmjL6bHKsDDn z;EN6lYr0m#bsHsf1v?>(ujS^G0mvl%8gpB+ zcMAsad&3GZvIMlh2DR@BlTp&+wL%n$6sIa}%2e9a`Sz@AYqnf$Oof4$po)ctK*~tK zpGjTpL#1SB^7oye$vZE-iofN(M+u*^Nq0>EHy<<|bdbFPgyQlm&`O=QyiNs1lpc16 zJ$IH1!Jn6I(KyK(q<#ASB)GNvG(!WObw0k>=zm}cI!ny}qIzlr)xDkqMQ;k!^VA)H zn`VX%`@HLF+fAlfv*XmZSN5or1j0;>qJX$&B)^;0VtLA2-*IUR08W_(2?PO6;L8&_ zv?7mF2M-^hYy?neGPa+|Y^oTswZ9EyOzD0Xt(N_{kDb^Lst}RWRv0S+YYQWKHICb4 z%#8Qe0|A(L**H^-J_gG|x)+vusEP3Gh@&dtNB4u)nR@T5Wsl2Xc|E#&GeF9 z9Ip@B7d&OoZzp07jFfavsHu^WG;^Jl^}+CMC(kB1tiHH(=)VoLZBH@kyx*GqdnL=4 zEjnH+Q5h!yep}}y)gv;yGVg-7Sl$w2H8)D60lKmz#Z=06;*{~Lr9-^mMcsW*fyJAw z%T0d{`&~hlxHo*y;?%aAfos|lrGp;}s@0?yv7zCwcTfrZ@;=x(*GK3xYVQltrJ^oU z>hlsf>(c-Li`Ttd1-0uAKy7RpjzzEg+Xbe;w*WC)WSu&0e8r%Vndu5m4b#euHjK&m zs*lckYlo!9ou{j5u09=>I9K13O-+kLpopVasWE&{_VzklhH<7yZp<;W^k&?P>TDC) zxRV5PRvx&eRWiuyVdA6jv!FN*ISJU{{@qAJ`AwqTBfBIdL)Xr5l|X5t3kF6Dv*xru z-{cpTv0EsCJQI#cNRk9Jnz;b-2tbjT*)+HM_7e(%0>}Lo29*Br_5fxI^2_8Mb8clE zr65V%FP3cwh>th=PyaC>Y^ChciagAw-AJ(d4DE^DdwbZZd?Zglf_GnhLy=NbMP126 zo54Uq@LUYVx%TI;qbzhVvob`>&Kqo_eyjoEiP4c<+RWLwtOO>`)%C{3LgSkKfpgxy zk|$I{d08rnMZ=rM$UPLVFZk*M&U<8o|2QJ-TM~=G1OZMb_w754BlSy1cQ-kh0Y_|9 zy!?O;ZP5OG{5cgdLrjPX(Ii?8+nTd;iO2F+dj6qUYk$g3>E|P1inZS3kw^qmQ-dGM z&AX}bp0!;wFpS0#bj_onnRb|%3+B3@_31wvVzZ4GF)R#i)KE+~JD*4V*7@j2OP{Im zt*~^o6kbZBIwY%iAYUUS_E28=ZSq7-?EXz%;6(Abz+Y)fJ;z4s0(pu&wFHGA5h-0p z`MeixFVo%lT=7I3*F1jD_%;GlOhRutH(aK-yH*iYEn1qXsPYVJL{i-g!&n%f7`H%_ z$4E%3T)SCq2S>as>ivtWpdU|;AhE6^WiwrFc+MzE|<=oTVH?|o6E z%?q8hp|0G=ki#B&V@W2{-J3GznCjjooyGO}J9mZRuF7}7)vZah>9=b8?@gWPr4u~n zS7F-^VWI2(wa-TE6W{!hj#qHpw8oJuO2K0FM`c;%kAl;^!Oebve826|$|mnj`FpPF z99Z*_KMaZl?jOv{l(g`GUk%Y_8d?O}uNxH9KnoaA@E3OVHFcoEYmmzG3DnAO_lY7e z2GAt0EUO$XzE&op+#G399iO{~=Om4>CvjH1_I6)ewZWOhLff-lTJ$-=U+D}cG|uxD_bDyT{T4N0f3}w!88{E;O&^t&QI=zI zcW%a#WC0g*xD;O(_sRVCj7}d<8j=t5?H>B5paGADU_Eos$Uv&Q%-xG01uA&H-4gAlzt*8% zqNfS{haW#UvyCP@jo)Zfp4|Iw*l(I_mf2IBQG_unnAYrNF7a&Hiw>)|q9Sn_$cM{> zatU*wZ2>c%J?uV0^f1qQ>RI{y&@rVq0Sm{7vVQ27MxAQ}&n=bd%`?<@czG<%{=di- zI}i=K`DinlS874L#Z7hJx5MZl;;1b?jZq zYjwvf1#5LphxnGXD1PcJWKKi?R<5l-$?`!lOt-(_mwECZwm)&TUG#d2)IG_i>Xl{K zeL&x_xay{(cXBTqzO#JU7X9s*uQ|=D)__2*@EPhM8B&kF0TA!XKsU8mhLUB_z1n7z zdZ!9glWj93#pmoK?j-fb(N64)59}(sBm4#kvI$TM^C7Ve1k<&-D&hiHFjC8>jB%N zm&u?psiUz39(6r9BEJG=Y-7@>>6yW%X344(ICF0o5AiXOoS#OJs070A=Ww4%oa z{HP`RHBpJUheDS~#~QZ)2R4>W_eE&sKKg!{`tPrAPJVijv^SnYV0N%JMeUCJ{*JtT z^gF>f7wP>kPNYiV=#jQ|U@Ek*_(1BcBk2dofjXPB45HabG^u#m>6txN6yNts;n?!zQX{G&1qa;eeBe^E<{m~N z<~oELFPUIIKr62fZZGb8nIRH|nv92dsT4ZvVjopU!hdGdP*b(#X4BP3!fAkx$we?V zKKQI$7cF3nEB$%6DKV0GuJ~^-^JM%-99$-uRYPhG%#(-cIf)TG18a`y)@WWKD`Ts# zXcnXZXDvov-S1y|yJN?Bu?Bza@c6(3EgA1_Nzb&ufq^bX!JoT&0LQd(eynJ_nnZT< znUK0QVR1u>)FS-Lf4ZJGjmzyx=ckGx{@pgaJG^EE@`3;9+kJFp83g{xd(TTM7{g!z z))t6pW&P@iGqvq&>c}zjxzQNyxq`w~cY^3vCMO9fJ;L9d&p;6yogEmsgy#T*xLR{s zaV*|2)ygSz3zrl`AgfHfxR#mV}|yC{DVSGcIX1*=SVHSEBe>Vy!s_uV%!v z+@}q?^)BAWy(rVb+PNI^ox2fATsmkRQBsanqweupbJJaOXgb9Z9*~rwMZKUrdHMck z6wfmISFf_J3jPjJ_B&NtrT@}D^ziCxts$clF@j*cg-pyt>L744IAL zZkC;t4-RW6iO(N>%`2k$olbI>#WWI|fuRZ?&eg4*C2)nhMx=XJ|3@!IFmuTqdsJ5U zi;+1nARpY}%>@H5N{j1ikp}hI1bY6PZZygl3@rT=ljmX|8?gOr-e>w!SWKFC0U&d# zN~Pu!G$SelAY(O9SwGYZH#)DF7JBT(fm!G544m3gQi+#oL17U&++MKRz+$ZDu$oR6 zB0yO`WjezsF$bY6S6#SmMjGImX*w3|OEOu)9^wKRE)@~BX=?-6;6dvgdCVh@WQ04-Z{^!@7UROPIl)u0J&|&pe(s^fDgIW6csm7O0s$06tKpd|vKmk~ZvJn`5Smk%)lqtKb-he(OW0EOQ z+iWvDPcCg6Gxqj|zmALkG0`u-e4_cS{cRk3u`ID}A)w&sE-+8TrY z;bRa%_Lh)GJzsRb9At8ZyWP=<^XcOv=diFGm4c47;NcEqpsaa(}2z~S?c@kZIk$NvDlvH2nzxeRq2l8CU-43En zg?W}Et5jdBQct@QtjRt+Cv%`mrLJrse7d^&xY|Mmo29CKZToFLv{dY2;o{&|JfFZ< zXaqx^&+FdJ0B+EWRL23VB?8IP=w3Vgni4`-mgZL_#gNU%=#`aa8?oh^#l&^*SSutn z)Hl+F$uA;5?w2|g!S8bp|`aVt;iT*N~*_3oM%9Y|B?cwJ^(&M6sI1lUx zmX38cdGi13^?}}lR7urdC!=pgR>#g);7%b|n|sd^V2G-)LhHBcn{7*{7M8Cm`SCr9 zLGbCA(rb^!2j#7Wo%8N%QeyHLgDNF9Yf(63cun?CxQ9WtgL#wa+o~ew8od+Gz#`;> zuf)1#AfO@CZmMmUj*t;wyfmjKEaa9a%muC3PEO#hlNmh6pdLj*4XQN)rq5AY6>W3B z>+u)y`Br3FPInwO+>QYD{}V7&PDFe#KW@PF&WP!&p|>w|mA1!6LiT+(TA|RjImXDM z`1MPQ+|ux*WRp?vs~crk4y@&-$&dV9ys!FkkwDA0EoneAEppae4q zSoYxw7S)ALj3v$yC)9RXEa~%r$1S*sLFXPG{W1)-$>J{*LnPQqXMO@YvHJzsqfLkJ z`gcUhokfuX1vT`%?{}Lxy@BDkHjl*w8b!u+#$Tr1tZ(#!Loa#vWV|SA&3EW3Hl#^_ zxaob~x2y(A**a-=Ib7|y4*}K(_HK9ou3A|?!f*c{Q*Rv=<@a#$4m}_-^neIMgLH^= z3kax`ba#jn(%m49fPge2DIpyRy^B}u90uW zgmAWlpXbu0_tK}aZdbPJrnS`3uf6Oi*_a5mbm{HNlr0i(cEW?81UKs)s7WY=Oeo3P ztJMXJV48KOVfw*PdrVw=Ps$eY7ilSVOLMyUy9qQCmIi?Y)B`oMzY&4OdtQqU$itNQ zW091Z5aWhwWLz(pTzfVtvQhW8G@6lgfTqXe-zG306Z%x(P|n!TpwSJXj-kjn=P$JZ zq-u6sc!BNhz;=R)ksHV_T(Ks!l4Af?)Ze}}2}Wbo)FR!DsMA+`oD=ym34dE_AIDB^ z;~ADfzl3sxlr#K`+Mvkx32Ku4C%my(?Nb9lI&dq+`0dILL3}AB|3ZwLZd+ z;j1)eXsYpmx)1h}HVVFb{!ubBM&DogYhoAvGMTSsAY2gU`MUy>8fIL*tApz(yv#F9 z=)Qe3Y%zCSdl2m_%XBLHlDultd=n&Y&Z{aoE9-Fo5&(j&z zln(!us#_@iHqm6L3Gs87(qJm5vD-RPRv9HBJu?)&m*pQ+4z$*@Nvq#J>({+q-lrBs z!%Oe?IZ~K}L<)++93DJls4gUk<_KJh0$YWChi_?KHXa%E`*NS>EatoFBcu06js4(Y zabmen^HrL2ql)dvyjkKZ-%Oh9Y`=4G)GpL0awj2MnFXa4hKVFD?bcl^lK*CLi0tn( z8s|EGE}TZ~;MT|KBKRnaE7C;qQdtxu3T_2X6r8U64R`V7`IiP&Qsx?2=K+FAt+2hBsTf^-9 zoLTj37j7Vo0z64?_xVKKm%xHq)53AkMn@`vJP4NR@u>dTHLr|UMun z?}gEu&aBMM*#5pO80ZV^MyvTsm}mVBs^B(^HqQl8257L7OELB?dz_ zW*e@17&^esuBtwtSD#!EuzP6B3m@tf$8M}_Qmbu}E-uLC ztEnd#VY)L8UJKmL?|P`DX|h5O4Ls)aKViuC`7ZVE3hrG2j*$e#-n_M@adGbU)&u+- z?zx(}db*z51tY!4T&Hy08LjKLbe0R;zk`*=Am6nhtD&zslqt9!_9TP$5f?PEhN~Ea zIGtg55cdLpmt*j6Elj_&^*T?B6np>XKpcDQ^{MSS=!4hq8j(i>I7qc%v`esWfVZ`j z;}yn1edJ3kuX5*8&%GT1|GgP;(w>;IkRgpFO{M#XL`?4^LGpBz`dmLfKN- z$R5qxPOQbF)Zxsq&D3f3a)~noyIebn==qnF#l19G~WD<*$Yl)w$D9{5_rNplx2} zdldYUd<>}6?Oxy{Im=U-fySuuzgrS6jvA%$>ISk7(GO8-@p&HpVeGc6 zL?MBHaV>vt8^ZPJ^;;vBZgB~8>ZHb<=6|RK0tJn1%>j_^J{LZxj4yv(YQ#uXc1jr6 zwsJC3C;p82dGzad6f)PW(^<;I%Yb_QvYBL9zjN{)a;s*}+lrqa^0%p1db88-fG@It z(X|t1N3o0Q&y}0B1WqBaxD)eZ;SVBQ4ZJdLi8qw@HEjQzNhErn{D*7@caj6vWlWOu zv$N)1eg9$Ey5)NII|2W4Le1<83G~&9)P4rD?kD0gHV!!AG*oa1L>$E!4n!EepYe8h zRJ37|Cu^KbayC3l0Uzz}36Im6#74&@p%QB2mnSuTa^eeN+%qvbWZP1dhvoCq4TZ>FTwpt-};^%P;%FI`x>0NSls--uEOPs&IhJ7TD6FXUv+Xr#dseK z1Puz9_}edjZ>;X1_~da59fm~aGa^GflUHjaqbepxSj_T<4`=ku>*tQI-mf>UN}R~| z=oXvE_g`q+oB6@fDJXM|tT{}4`{jO@>wagALL)qm9`@O(94^yc0Q}! zr5gP7CB&1im?_Y(g<{gSdfq%_i+tiq@vtca+(B4)os}EeZC0h**r>~=hCe5gnQzlN z;XzT;o--xDdmeB?Kenr)u6Enc-}^E<>oib@Z1Zn-+68}TeRiMjXbiG+FU22lq=2eEl@t?tIOFHI z*icpfI!uB$rhzewPIWr<1Nt8Y(*Mz>Kq|PXVMdh<6?ki5VgVX|z(mk&@?&4*te#{A zg$~WX$~ep2%ximf&@jy!WKz7sD6ss*H!u-l=S$tJvb=(E?CEBEjayn#J9xuZR$lfu z6&&Z883A|)@4ROn8V8hme3QT$yX{~Km<~uLj^%IJ$37H2kg39!4a67Nz-^a@4qoYU zY0fyVuu4%3!{o|ZRIo#(twwyC8Od!GtuE$9R>Ggc5Tzyp-XE!$AITj)xwO)fgxYxc zY@Xs3r=e(bRykzV2pj08JjfHKOWSDC!liRJeHcU4_&F4Zteb3adG_2?L9)NQK=h(O z{QTpH&;P;=y}Rl+2RZ0>HwOBym%UaJIO2Pyfh|bgg^u*fS_+3Mm@etm;oVMHsaVXM^Wi4TP+BY2}oRflHdm6^f#yS8D>OaYo<*1t=L>p74^0Q^ePH2AqshIV` z{LlyUy_t)I$@IzeCdE+p<`zU$$tLdraU3x{V>UWjzeAmwSixQE2lSZ0B?ufW&;gEr zGBe-#4Z!>Xo{5>>flJvpc73~=+QW3g`e07DS#u1^6_AS{SL|Poc3+^l-jT?`c^-!T zy+XLyGnKjH`G0;0$f5nSXUFd%lmm>HWp5|mu|6I%lsO~C!Q+vP%DqCQ@f*Yh>jvu} z!;GVB4k#=4-@1N_T9UM5uZcO~L0k3iI%6Z{L{OuW27HJ5at-rpgGK*$eG#=&3}5Nx zQKBrd(5HDHQ^GGzz9K1x=3EVp@^AGFjQWRRA_Yp5mwX4~J7Dc?okC^8vL;I&*=6AC z@m-M{v;UqVo3}cF(zfF>MtSsVkzAyc2hi-s9O3(}c-LXkbLwx4yFWW^$MuC}s{&)R zfkMr|e}l5PY3H+rr+6y4IAPOunUzVr4v zOfGsXV#Iewdg2>^`rVY9dxra%s%S}aD)uRfHNt*}7)s5awCf0HJdPEf*&tXbSF6n0 zGS@_bKC@MVvAvmoQDT%DHT8bc`xRBiZhF6|cZcoy65n|wLSKD`;i0w!S^@x^S*sqS z+l1!PX`Rcdu4w7_X+4+_n2`L9jLmw%B6Mn}V@`EB7b8b0V5>J4U@vjLVIx$Dbq4*; zL9n?~uUWI|pL2GngkcmO)PaAq5r(D|J~aFW?hZu%VdqaYo9l2eIG12QLY|js}LJj|2A+QC%jz9T=M5(_cREN zkgkNpzaV8p1Ucx92e8a%qe-Q4Av#Ti zJW8^Jl6D{m7AX{M*)zsMpQF#rI!NaDI;r|1pptou@r@ewyHZ`rQ9;{j# zg@dS<5W~JyJ4MJ4DBaSQ3EZ7u^6f49TKx}djYnN_V}^XXtm{tyvm?R6_NXo+^Y+su ziP-5wAM?F&i`VXLZ?e$+T9DSvb}PK>4;v%}AAyB0+MuzkKNzA@}ucxRuH zrx+#{CoTmA$Kcc|)!Q2Zps>?3AqBmCX0*KFQFph82z`F*-;JofDwAiZW_9any!r_@lLI+I2k8Fzc_|$=e z4?zK-&t2ojeAmQ4)9j92r|R|@%1I2$qI2Es)iRT+uurW1^M3RQzbFAUK;Bm7tG5&AVA#QSSI>`R44W?V^3qpbUx3cQgig zg$n?*uA-_cN)y6^F7A2SDwf?Zx~akEpGPfU?fyZ=%=|HGUy|Z=E`E9!2G6CsEWHzs zrbi0zd_AQ21<2*QJ@pX7i`=|>CNVt2C$~&Vm7{FhOJZc0ck)^(pe9cz>^Oty+$r@P z)fnNvU4{%l(&9&rC`K^qk!^^_E&pSr?NbTKNj^!r!b_78EnKqm zu$(mU6zcl9Nh?F)=VebBRTkr&z>snM`S-t z&AKi1S*g9z!KRHaviFkxIV2uWCoRW62sO@wGPIwEj zXjFp~$Ov3zE=>`&qKs~RlF)$~LtRrwZ7$&!WP3m914h)IG1zh0!=pFXncP@F^_0!% z>9vVe=z?F_b#mM+`w^r%X4sQplF`X&0+_R%HAJ8!Ppi_9a~7Z5hOdXUYf;s&f7WS zYAya+;IeQWHFWAg_b>eIyP2e!*#y#CO8d5QGt3jO8iOinY2Ec3VF6J2lVFey5V^3?4P%|UK(mTKvx7RP6TNMWHN(~+ zCMDu^>$jFtDaER`R&g(evGtmuY3hKa<(NUvGpBvWMdzE6$S34pD|py{pD{zQ*Xcrk zinX?nd_>8>S8f^<7h5HgeOS*TXX~5ugwFqh;OAn{5+h;A-QX^8z@Q8Sll`B;+iN#? zE?7OgV;O<8KWT>QThkB9ZQb z%dL;hsFh%9O(Hw7y)9VN(7rlMxK{++U8(zbSV6D1PUQKj)Ot|6S=yb+i(-CGX$mVT z=(XMVq>(>AWC)4Yy!tZd0$XNq-d4=lox-@#-A+DXV3!c$H#u}EC(#Z++4`g)9&lT`H7Wm_c=Gp)!dc57 z`|moH>Lbn|U8=pfR{(2W4%b--_@apa_R8 zk7M@qX+?R2{Tv}~#0$qCkq#6~3~5D&I^QxPy@L3y!jaM+(E=5E`R{NbY|D-flNyz9 z&cb^V>;;lnG1+C5>3XVohpSJLxjoxl>A7aKw$x?l1;gZWU(|AwDO29YB3|{k=f5Ww z1kVbPx`{VOtk~3)5{P25A2QVe1&hpaGoeyq#wv4q*4;NX>|D$8D#$*jR{ZGA8GhHh zY(jd}^W;XyGGKfHA_F5H5-f#hOQ-VTN_Q9El&(z;)s9}$^Bk#TAD98wB-6j`)ayd?sp$3JDp$@qSUCZZmK({058Ry2Q+u>TlFXXZzp; zbJIBy%Y#M_!15Z(`-xL=BT(&{LnN`qdAjU;`LQ|hULF2%AeSMFDdrKo=JCa5M1|nZ zs&lQB;5@L!^?n%4s1!J)gTC5h^l($ti?EFf-f1dU;dXp7Ng#P+in^fiYU$<;Lub-z z71i4d%*XHL;*zq(KE58tihordFKnRL6tYTA68GX&vtk%)Wu3_ID zPLxp(xnko}Lj6kQQk|a}PH`q`AP$WHdQ?7h{?u`K8LA$Svme7difzdULLo~lsOv3+q*?k;1!~9q8Q37G#?rvk{3f?%Uv_7T$dG^6=vvh4w28ma$>lD8iMYNXzsu2(8&|r z?+HACztd=9x5YwmS#tU?g9%d7MxKkK( zH7su=^x45K52#rpl4R`9i$Rh&%Dk5Tq!rsv8qB8hCziVhVPPup(~Mz6eJy!tKg%-D zvm->6#-BeExv1^5@V{GUA%89%wEOxFSm0q<4^gcs==iPHRaX%;e+{??YK42qOx`mM zWW4wy@>|_6Bq=t zzQ>%NjMV<&81k0SA&7k*i{^14C6-|TMm481h(&8d(|GcFf135k=wZt>m_gZQlw}!t~cVh#q-C-M2^G^HRN0?fiMYU41dt=(N`*5aebKQ zr_w?lxCYB(ZFp<^ciF?U-G6_GN4(2B=fvGVRwAmyjbw#M%5=zxQ1>L`QSbw@sAXmp z+;nIKk$%^V+v-n@yZz5+-m09G#Gx<02!}8;m-Lr59R2Ps8Z^{<$h#ik?b?zo5sLJ~ zIH|(^nj`da9h^68`1DCM%x|GBfEK=G*N#d${4S@7R>Z0pR}AH+Fa|y}mBQOKR2~VE zH5Nd7Hr?$U{(3_d%Ri8|1Bn{-+_Twc@#=?9&2cAanPh=yl)a%N9iOuBHAKKoBMQF^ zCswBV$q2DZQS)ITBzWOt{sPxX4Bz1+&8$bBID_674d%D2j#S$XW? zdI6U5ZqRpPht<%pUQ~oshRETfR16#lw?|=R>Dbo;KOTAfFzVN+9ZXtv7$nt&4&;We z4?o``GLZ+8PeYSh<9Ge0GI?I|)gDF$az(9P0d>mO6Pz=66gt1fuLnq8mxK7GZMBnC z55k?ac^jkX3FHhFiw`6@x!zj?3dMIx?KLGj29uLIKRn1>3E6C6Vy}(r-r*)`kJ{<}jUf3ovO^=;!rHYv zvoFziFhdiep2nUgVK?BFAyBe}Y}brU2Z2ya(~5WZycJKHs19zR!iWm^(NPst`}q^- zfco*qquJ`}Vn##2iP^c04%)}DT^@#D(X1qgw&E>t{&crDP8 zZ1lG2cYDn)pl)k|35I-wgxaWHsISdKx_cw+cEQ*a<^Rxg>zCik12XIFpQxQkN-Ek~ znpY>``t`fB0|4;=5uK8%3J%@$IdcBsKp2=iy~;--@OLz?2O*QD^)K9LtF@SLn_Nk-ee9$1NJ#-5S1cJi48h(rj*jaV z0t@a*zgC~55}%9zyI23V&2IDeUlaU+91_M6qcApElNe%_$FkibINC)sr_=LAX(HOD z{~^%K2D~fk%lkwOOm%*~g0|K+PEL8P{+vjY72j6&CFf-S#zydhXQR;G7e&sSm+H!{ zP)RHGs~MwU`KCax5_*pQaM8Mclpw-#&U=e*rft&;$+zY?78I#9DEG4McyjZte)d;C zgcH<#tKblmLY`Je$T>`?+w~9?se532m)$9CDb| z8~B^QVS&Yj1f8&1zxe!V(C5|!JhKFfm~?XkG4j!z4v=GdAFAG5dA1x?6+7iD^uoLU zl}V}HftkqK?yGWu+!8|j-EYl~$9*3uBEs*Ye>_xA;VPRK*K48vf)X4T{$$2+uPUzK z^@sMcM5R z+AGFow{mwOE#L7SERi9Gl0czPE!|ESIW*}VJW|H-Pw)J)W{E*QZpT9~lolopi^fkFhLw4F_vH(tuRnH7VOFLtVD@=Z(ji#f-DcdDZIj~C#3x@Ual7ZN zv!(CazaFlIQXr(?|H^;e9|YY|5p*P|LWn@pY`tOETwHG+7UT`LiCR&D(kZ@|0s zf8xY{i=qIYNb+tZjMczba-^wY-9;I2Wchy4AlIKV;WDSab;>L}lP1-Ue_FjZGuO|h z|1w@C!YzkWea?D84VqN}1l2?mVgjIxp`6Kb=f&$#0rm5EO z77Ko0Wi(X}x><%z&~cdY)joj5jB33*rK*Fbjl@aQh<9bT`xKb zWE2shrnG|`($>}L3~SPO#+df!m&68X8#O&fHLmUxsXlIWNo!=-)WtTxL*LFu>eTKj z)))BkcSN<)GU`O!4#c?ii)I^6hh}D|8Un=gyZ9}|rUGeu*;{j~o@sM}1SWdC`C#)e zEmK?-rLNqRmm4Z=T)TRu@gMHd8ncMzlN^FH>iqC^gV^ z-)2?}d7S4k_>`3Lk*90(mFuVah4OFk3|-&!LZ~4V`#5PvvwmZPjN&7RaXJCwO5{np zAVP(;W?@x!Q(fA$D0dK%l&Ffrv(LkPZ)U!tGz9?#=e}w1skXh)&=mK@aQ^!f>S0#Q z?e?6XTy$6&xZa|yDnwt~o*Z2)WFSPbpZtN|Xnu!D(n?Q;mYC=fN+LS(=qWExr-x!q zkRXsG0^_ZF=KTO4BUq=Uee8KFA{d>B_Kx$_GLw-XS8FhK$h&EfVvNSjnS8H=T-F~N ziTx;(E=PpBFP0Fe$6R1#W7Pp2Gwv>gsx_%};+^iAs3W=U{sk0<08oW(5r9*Hq)Op3 z^ok_aagfxnB=for9nNvh=~lTatz;DVjKHN5*nSp3I`xO38VezO+P_>Waf4?^@QqGtTtM z!@PnDG!k5v-hTuyFC?dS3-PE7Yiuo?I2fmXe68#v2{{`U@_=-tL7|;GAhV{ro6IC? zau@q#tS=Czi)cj?6ZIjKVku#@Odx;}@CRt-fU&~C>RVp@L|^Gyed6p^#4ZO`VAATe zM`S?szD+K&;T#IXiFR_zr9EMJF?Mi%UeuGmWo3y_Xf;av&@hFDSxCGJRBAErtA6c$ zAzJ@2DygjG{+ajjFqP6qO|?g{7Xpxr)3c9vsheUsmR!%O6QJToQdJ_*`%{ zwsxC091+S#3&jLV^IjF(MvWx2JxILG)|vbVWnJW1DAYLGK>Wd1Wj3R4gXhB^%t1Op zoxSZZ%t%@>r{=p-ZJBkYpd?gW&Te>dlD4T%Tx5N$O)`?av;5MkxQ-ZD8Lh|)Emz@B z`(VC89WAdGT zsC+1>ehk9y?;3y2mCxWvf`GEyF7>%*v2q00L_Qbb_m~Zzhmk8r5G$k| zRqP8K;(MBH=TcJLWN;(&LQSyjmJ;qb z9^`O1g#>*rixJ`B1atS3hq5q-#Bv#o+9cEsy1U`N&3W$*0XK`C;As?RDdR`2!|5q?cAd2#-FSiPwfgDXl4R7b-QV@oXv&K47DN z__&KNj_&dJqg3oQE1n~KXlzGxjk6CNC$KsZ(6#7ZbmjV^dET`&c*O|4*HDccM^X0EO`Ey z+_cC5%2hEE=kVVOGG>G$3w^0idKsr_>y+5dZp*DmU-4H76R<=15g&F+q{C> zwXUpPzFaVfBn0t1Qn7p`|6E6=g_Xueuoa)@FDvc{ z?qKSXJ5Jiye#$5#Qjd8S&LVAV=T{tS+S?pUI&ttU*t54F-pMXg6cLiAv# zg^iO?Z#(0rnu!zopB`1PMw7RHhYY_6;8mNyQcZd;b|~&OkncxB&XoOsmdgFgR7pCZ zeN`%Cf;Cv}>}ew9>&ghN;?E{>O`f?8>fOI>iQ-!QoPcM|e=b#OH%JJU=>)Y02cCB~ zdk4*vZj~b0)1A6M4wt`@I z+z3i9Kv58na6=%LcOyj|zSc8bswSI;F~&$La`kaZldbUXRn>w1XOHV&Pt#;{>~JBC zo~WfZU>mF7MV|Hy@=ZzjnfdJ6A(VfYci;LH>X@VslK6fHrDzwKcrJv}?U+~~4 z#Vls-E#~Lo-{3Lyf4SBz=k$Sm) z(*Hh_M1->Han!tLiRml+7*9a)PSDu`QPD`D=}w!6O-?wRT1xqg%V}a@ zy3kHk($&6CB)>o%yaJU+T@CN#M^0ZUfdKEgHp~Vh2k%*N62TylusccQDZ(!9aS;CB zp7bABvhMUh5h4KWD>|OD6JjtcN3n!vQLF~Oi~Yhb??5jHUy^Sr$M>W3^bJ*J`F#93 zNBzuJ<8N`P7b4J5_wSp)U`T}9qdu$3IT<{qt-QC8PI1n!Cp8N?DFb~P`l)T!$-4ZU z%Js25(3?Yx2GXvVeM{ZXsp!1cYm2KL<KN;L#^K92k zKF-~|A^W($9z#yg+H(xMNI}o~qUbi``q`@vReDj0HvBDm5Vk6r_IGZS%Y$u5eskVK zM*btjcicd!*&?#|;|(Sd|)0fGIQke#teS* z^cko~F4oYvwp_2#M0Ekh73I!&?p2qS*_d!smHOjv5_a;~`mA-bGB=x|yP^wK=Q{mP z_@s?A;UHBs(248VNIA!+Hr_sT;-Jhzd=JSKTeS~af_AdOPm_t3!>B!|uH7XN3A;0e zTio1k;?q8AeZDE)U`1J>7qP*kI#NjH0+cT3mtuW=vcouPIB3Kr8aHCfbx8dsLiZlD zA+jA}yCtMYH3+l03+sya2`O<8O6Q zvAGP%!?c30Wgg=7VYX}Q5$s6@TCO8ra%MLS)QrA4X!Po{tvt0ZaCf^8?`T&MsVN|@ zaOJLFh&D2DnTGgB{w}?hUe!GtTgqa8^QwcKDX<~x^zY|)fb#F#oe3Fj2g@lwp?-VW zS^An9-luoJ$AR#|mD@ga+2Ut46lXQ?I^;c!G10!K-2vT)<(5VE`?hy|qlQ>_@Zsvf zk$n#tk`=3k<|u5}EY{bOb2cwK`TPOk8d%T&7dQul*Z@1`5^FTxvluzzSLt0WwIX!y zkORnQVL_X6kI7%|#Rkma?Aq91>pnK>kK)D`ba_7-LWvtJ`mxH#V9eV-{v}Dg^@30o)n?7Q6 zKs7cNsa;#9#KHb6mjZ{sqv$;Tgv#84_v^eH@sU#4Jb0yZzuvoP9Gtw42YKj9L^+i8LX#A*T5t>yFhX=Z+JuWq2da01i;Sg%SUTYAvW5Jel&q_ zAP$UXlOa-EzebP6A=zhsDHGOcYzPjt!_|J7^XrX33~b z3o`zW8}9l=AY5?l31rFHKiYb&Q=zFy$Sv_iB-Wj}30o;JWQgT3DB^v(lRy;r(Jfke z;PsoY<9Dj~917Y#_K7c2+rk? z-JZF}MdYYIon=`_6ZPE-9gI3`T#`3wI|Vie^d0YR>g(TmZt)2j!}-eq&(nOTe>Hq& z^jg4=nVEHft?63r^hl5`rQF|%)9lGD>W^R9cwlLIy#l6yNBXE$A-FlorG=9jK$ zT%T{q1!6R6C-2?c2{^3(v@Y7^e4%#UtpR*Xi+&rYS-c-Lo6$a+EW{+~pjW94r%_$B zcaCf4wPO=`J&!3ORQZW?6xHN`~o(=E|7R$Xmc#5_OI^00NA>5r<()Cf1)4BcT@PINB zR%T6QwI%yRILm`dL+P*)95J-D$n$_X+nh=LK!%J;MTFs-i6Zv`O>F~K)(OzYF>!_j zU~w+L^eITh4$ay+Et)lr`FEs{>SN}dhYBmk54kV)u(>b< zv`M%!=e45GneRjO_n<&?_QU*Jmitd@+@kN#7J4~*8a66<7_mAwZSjMb@gR_ha_4KI z07sxx%cB`vdi4aX68N{d=)#qfF#P9LCJ$!jj!`$ENNpyC7>wqtbj71;=44PF|K>m8(DX=Ajn7CmuzaTu%ezBc?eBjL+mO8@Yq@D|LG_qS~<0h>PtTku68v5QP) zVug32a$W0}KMr6ScYi?3Ok}uw)tV0bxfh-L3`ROY!<~w$sXl>H;rU6qQ0wGfvt3=ETHdtQ9(xv2@Y(V8H#JZvf>5-wH*3``Ce%~loZ-oN zi@4L}#JPzf#GTC3uH>QIPxq?x`@s0Pu9eV0Q9?qyR~Xz0z-#Gue~zN87%6?@Mr9?K zj7vD*MqxI`AmY$&u=+HTnJq&{^!y^)ZI9hic>e-yOOqXkX8s0D-UvmbKX`g}M0&Q+ z1p28b<?+_xRJ=xHl;VhLpK;G}3eC`lfm+5ET0$2Xoz9I{CI(vX1HE{Bv4iMzglH82Ib*Z& zpxRX{O>9vA0XoSw`ouA%PsKJ^`|v!-_oTv{sd0E6^DXSI^{ADi-?l`n>{dWYLvW&C zve?(IH=j0zpxcZw$?N4;j9CizTq#HrPJvFy&CKytQs_YPE|8vb(1( zXKD9}{&xo*#y$tv*;z^dCfuDmoxvF+Lal94EB2hZet*@KYAUx<+-XmA^^Amz`qHpo z61wlEoh$buK!E)`8IE`ao!q2Ec~CPLq5X6Beex!u6e0A|*%1<`3YXLv5lK!ibNgMY=h?f-IvSD8uy6<5xl-#bHI{)FWn<|uv1_o=Su zauSnza(1Mz4|_J)Qm0<;9#sz_0{_aQ7OYNKI8F{xTJ=DWCOMS|QN*ZmR#!ix;CK;z zqx!IWo5Mxq*;?;$jQI*8{3UM34_TiFJnUmQ<+4IAGZ=i-p&J*VJW2=m9L!`(x|{>L z&2Hz)pvp@_WErjnX+hns`Yxc6i-y999uZcc!I8mZZHA!Rts>t%&G@qYQB!RDj0mfc z4p~cQz%@ZV?kNtBcg^STXVHSMGZgP6+~)u7F?Q~>p5YjFDxs9fHS_f}HY5BRoYDzwxeqK4`cIQqohWT>ea`h3zoSvbXC9{oob5lP0zJIO zX~>;PFl}xi0_{BI7#@Fyh?%vNiWq*m`1XT3DnZUz(lWo>Oqh@@*PvA-pT(R20EAtB z)YtAhu@2NM>DskEnw+tH&*z-fh2B+*5%QYj8=J7pw=&JH6m(yR$vrjEE7igAT}L72 zy#3i69s3i+gbsC1W2&^kDKD9Af?;W$SISi>D$#v{oI2Mo=Q8?bUZ+G#z`JUH|1S9> zPTOdOI6PJlyAWjwQEC;Zjlb%aah5d983xQH>@?I#t4~O+_d;d?2thrUbM4t0G+CYE zLTMx)Wj$lHp1oKOT4}7@yvNA;-c=eGtVQc0q{0m@-qUj5v)$$qkvV( zW8G-*9zK64OlF7ig5H)>`8TK2FI&0NiNtdqMwMxN1}s6kD0=@BceEDr4)VQEZ&Hhr zz|K(L&ly0Wh7GTfH|Kt&u3i}}Mbt+oE|w?l;wWrl2Et-Q+hdF?S0Cn{C>zUZ)F4GF zW4pg5qvf7e?ROCL4+kN}_XZg;t^^Ff5boJ=4IE}mpiyW?QwRgta+CYGW`1_V25xIL z6bxV0SW^B~+iTbQVqy}>i<7lvGF8W03GboEeP(>Z{{mAfpuJ_+7hxP#P0HZKPyg&C zBB7x`DS)x!KTGzg?w%U}q+ci=h2l2~lR}|Z)}{cIxc4UDt?9p;y5%zj<7kx)sb^n# zHSmGCCkaK5>#8SrpqpcGRp3~7pDPQqCG0&_#WS20TZ?Wdju3AhsJXz)id7fZeV&1u$$h?kVFz68erJJI zDJ(ZADD-^tO&3vzj(UEprI}AkvI%yB-LmJVWx>IUJuP&Ck79_~SZv{f%bDw`+IVgj zhb_~gB)&&=gYWOZ&_AK#vh*H#ZDl5-alFEN5^1ov_m;nCtZ_^g~xiH37SoS-w7rc>5ML5%Qv=;3%~1KxwTC*pzL!$OeOnu^KyI*(dheT~_H#d}XJd}G@gCjdvW z^{0*#2=v8_vPR=i2Eo8}I6GgkUGOp;dkp+vHerv1Vm8UTudld8s1MWQN*b78#wC+= z&9#TWp3a^>_0Zz9#t*JXp{!~b+5mta=7?_o}DDb#3_LsCm zw3=y(N)$&`x7JI32v@|J9d&>I`B8gsj#k(h7E&^N?W4zbfS^jo_HBSeEzlnkD4@~~ z*yB9u>5x~Jm*~G*NTM@-to8g<&PP%#p`@zUbl>No?vBBr{GFBSy?TcIuoHDP{XG1M zwq|OPzW&d6*GCF}6@f4gns~ho_!qXkI3b<-lV|mOj=GARk`Op8>vFFtO)80KUyoP8 zAG8UV5NgEw{5u`CO?%L%ug;E~IkF=O%A4`!g!>=`UP-+95^yGj;~ip597+u*jd&FM z1hi~bm>fJ!3jn<@P=FT6-6#&Y+!piX6fr{F|m%F$Yb&f`k~p;@E`Fi2XvF< z3dWfy5)NIN?3rP^N!c*5S_-S?`xQ2Xs;xi!pBN2LzWcDu=p!mYTUB-zG>q0b6G&cd z<|EX{xfUvG+{Yd6L_DYv-<6%VUdHqyUESjuy^WfF`^M^Vw@TYRioexDl0E2A{3d7m z6*qeJ z(=1M{G2Q9KzWz~>QyS;+l9(>gGL};mh&|Re;fg>{miOf@Wx-!}8pQ7SuPxxd<{5}_ z|51La*MF4?evS@X(7n{xpqR8lwI%)2C4h^@?H2G(pIG`n;y2n@+Hw807A^uONB zsO7%vC(hAFLgxBj@^gd!{jIW=5;`0oAFb5yZlHDI#UAj*iDF2 ze{8PEN;Pc|GX4-T6rre_L1gFpJG3sS**V_2M@d*N~77( z;~y2HZ08-<_u_q|CDE_f(ehB&y|-Xn+=_8-^kWzU7kvjGivcu`O>F3M98*Dn_}f18 zm{jA`+KTB|w{sqP-oxlv!HDg$69ix(bnHe5e`k`T=zbCsB9O~a93lHdRGSrZ=aIP? zFT`|on|ADKd1#mNilriLV0C_vs$tQsk_}FEqVQ}<9h_5DStk7Sd@L8hWMwkc*@i~k zke3^gF^3W2*gOei>%pcy5-PtWnyNl<$(c3M56-o=ra1FBj(v6m$EAeJyez%@*PGyv zt#(Ij`e#iXou|(IsGtz0ZtfXRCuRj1ff^|8 z75|*V$`*_L(udqA`21*-({IssxP?3|Je-lj7i4~J;zd8S~2SN;^ zz=5H&=RcD6j3NOoZ=AiW-81%>UePZ2l894M?4>{-0SYLod@u-HJTs5xX|IO*tBsJe zw*j>anOFz{PwqQ6I&b>sGRD1v?F4P?nPz{-MF-Gpruz!&SF;qMH4D}yqQ?y_Mo5s% zc0fR_BTqb0gieG4hGnpHD}kVmKfsv6Xjb4)WK7l~l;0qlx9F*M{(2yFDf8g1>@NrI zJ~A9PxqW6dvVJ+Xb*=aR4fkkd`L`KZ-QxHM#HKMQb}=H2K5bt=MxZMkIjB;)<>a&l z>`PRitQPEr)glIf_Gb=^cxvx9ZmuO=T#sk`EV2rrx5_gqvTbw9$($hHRqfsG+r?C&nw9kBFt5Y zqVG#UQ(C#;w)4SOx-^LNm;Er!@NH`2J?@=~4Rg3rh_m95#H4X~%Vs%RbFl=rx2xXy zV^V31l?3$kevJ(iw(iKT&#P@R9*qB-;G1+_LGMX*jtOu6ZedgVgoH zk3<3SG45-Iq>@j5xWTWCUEZbLq!P&yZOV1UWM;xYXNm-{qG_Zqt+GV(_KkgcX>}*c zpyitItmBq*oGQ|uZq?e>pUE{;V#um8Ft=W7p)`BUr9foxzDbzk~8_myQT=&xWjYEZJQr ze)mR>KRYjL zfHk=OnVH5xLwz@I&oe<-mUr~Cn&izV$qpW^a>~4Oc(d=9hk@}A7(yN{_FVdcvmoL{ z49?Hw`ejVoAz0554Z_~6usv;~qP}4Ioh74xV(^@u9cAE7QDqea8(!{AuF%nez-rRU zwP7S^1aVy{XLI4mfV)vt#;s+!;;=bSg9TUZSODhdhi$YY-74FdKM_OOH%QT@GS|Hx z3XsckZ=%kuoIdqbnQ;C(t?7>*UB0pf?}vW?3%{G+wxP`#m&QlZK5NDj)1W#7xvB<{qa$ z>NmSfpD$0U`mc-c@3Q8vr*CYfx^>55lmW9>fI*3=K8$kJ!Rt$yYmXVo6MRwd5H~1; z?o!hWZ-rFNQCh#t(O#y!?+OAXmNOz3>}IX2VB4Nzkk+sS@Z@$segF>G`}~w+R~}ai z-}R`#i(?~+M;LzWbU{y|POcI_?_t-ZVb|?bD?-SL^9Xd?`)?W+(4uPOm9l#;hXZ?%dMdQcGZ zf>NboY*mEoA{nB-6MP6|BX`t2Il&O?iKOYp6B8$+o?=?}JERkt$NSU+lal4Bok@55 z2kkQTPuBn63Bi2pYDlV*&(LQo%M%xoPl}IVxr6B{12`v0@Ko@&;R{GI!>yG`}^$t?19d^9Qk7z+k{Jn6R<{a*gdox63r7Slsg zz-diGXQ|%?dWAi^I3HY4xzgaiygaMO>xT0AiH033Gav-Ee?v}*CyQcRpb8C$sHja) z;F>NE@7u_I!Zql2Ldo#G-mq^jhv-x%;s3XR>1$V+80%HQz9J2OYkI;VW1p$e5iWf= zz&Mp}!Sj7IiauFJEf(ROyU|2%?g4}?4=M0Tfq_Om9MW&BmZu?%MN9FaYMXNp* zbKJd=2>5JXJhtnNO05var^#GAG@+67A_P1<*+Fie5jp5A2+{>JSj_LTarySCaq--v z-OR|mTAj$@7xYbcl;40&cnf*;Ru&eS$rm>ISTWd_5OIa=y`K(9pa;Wk@uFcH+>{QQ zLmr^({u}6k7n`b;126}`s}GD^v!hUwm_g%pQA@o*PB-1_=CsQDk78)NIztWdlzvU? zQLyiqX++VGDsOYRWSdFRDV6_DKZZGeK;i{NHW8-h0;mIA;K)wdDFye)cRbo*6kO3H zG>n_P89Yxl9fanz?Z4b}#CEp9!CAn2tw}%rLV?98#OF_AI)OlP6&5#tjxLT;HzlSE z#Q>H`_v#-$)!)gEXPXcFQ2-<9@S)~p-=^eMSNwQ@s?M8Y5$D*Qd9C}ZK}2GQEXpzS zi@r-V(<9)XdPZPnpIX-zXYuie&~h;wjlT8xDI zGsB<*O#xLUJ1JsHUuanM0Kq7^2=N_a0O<{44)eV}FUetBR&;ZE@WEI5RVE3)V;=K+ zHIl>dTjfDcf2NCgG^hV=wn5H)jxh&JCkyNBYh^LccqJ$mz1j~8dCn*RwGB~ciEL$sXA*J>NKnDtb6_KW9r3Za$AqT^GWQIx!Rl56101uOFK(x_}?7>hU&=<5kVmt1nkCL}UMv~TzB z5z6!wh6Iq)i&VI6o76q3TvRh|d}kmCNJzhznolpWmJe(X9Y!u}Km`;SIZ=j4%fcZN zCT>lO+?)V*oaZ%?C>wKfwK;|>DxDqTl@QlmA4fnyN)zEYeo2^{i6~yjm<6DA%9YYo zrnSeAa!96p=a89VOsP*#fUXrLasLP5)nq&6OKbBP1UY2g{6czc-mzO!etZ)v-LW(q9UL^9xwo+e+?wLkhhTyknDN{|n3kb8@;M)OMDC}y_2%$Z3G3NDxZc~Iqe zx)f4V!^%q-8Q~v5Ce++X9gWMy=~Vg(2w|CT+v621ZbFAR6ci0iuggXv@o5dwqy)Zx zsjAQh_JL<&w1#L;(~%LkpuPJ;9tH=D7LV2_ zs@A05y4Zr~@qnpUq;QcWHXo>{0EI!8Gb2wuU$4GsZ@>=lYyp6IsR+-5SuxWz^S8$eo%@*Uz8I7b^UC7d>l?f>Vk?_tSRSFgNTR#!G=ei z2vBYGSSm=QeA54xQ`AV z=cMeMWyYvIo-c3yl>TVCH9tOlqm(0rJ^Q2sz(m`(>XxPlkf4ab-=x@?_BEch7=LSD zE5Cw*PjgC^C>?3Ip=#z#Kx6+dkMAvW=3AD`Hz9>nTSIHH4f`N)>r8%F|D7Au_6389 zwe4{o@C|t_@JV3;O<9NYtLF4}W{Wrm849Eau=YuSjoP>ro=EB4+b^W07EvxI&7=`g z6nAhcD&M}TbIP2*{YTWxWuYl^U}N8-V(?|tzZ5!>B!rEq<^-&oS?+$)SHFTo z5nj9lQz}x*5|Zsg)8Fc_JSYuOTDd=oX`e*ARPm6xGVCSxuRrr`NKf8X2aEbiBnVV7 zJfRkkGK0?{levH(R;Vk*H8X)w?8Xi<+j=rifqnCTwc2Oj+O;E#NA1rqd3ChMy!2#I ztzdc6%GNAQ)KFD(mS_{bu$+u3YUxXwxWJe-`TG9M@=&ZD(G^~}mb!TbiLXyQYp$<- zYIa=p?E#^)QxOvddzwp!b5SEy`!Q=Of)7Jvw0-ed^bOQ_0S+&c>jM`W$;(JomQnLL z6iPbFUFmy{NycdRKOb89vz8FS!18DzdcyqTZstbKuI+|mr&$fF;Y<`|`WZW?(O%am zT&Kx}u$b-3Z0(S)9(HTxe$MohBm^5VBiRTLvssWJ$`pVC%^wlR~u5=6Nv$|qL8G@Wnejrh7o}0GXG?1a6zaj!$ z-4`S4=$&P-)f zn~_LQIED<99N>>@ zxf9&Ym!s1YPsPL<2e0-DZI)c#Q)t=<}uu{f-Z*#Cfw&=^yBew*aTw^BEw3DooW z^ZPw0?cnScb3s5<`?^ey4amQ{zVhQ3ky`JQ8EdUl0nEK=byGgr)anpVfP0sM?~#p5 ze{Xv{e$rNwuuE0Bmg|CEyOwe`Fuw~tT>#vh#nYI;)yNyN#$RI+TW-y0NRl&h`-33i zBDXIOfz2`X65F(WPC!59VYNb)x$r6SW@rwqN4*p>S(Q{cMoo4$g~`G9GXvp&A`S~bQX?R_ z1eWplI{kZ|=O%1M_4&)@ZwF)p4!{BRdN`7t~i3T6g`WCura zl#Degl|y`(-ZQ+9mB2NB{p|-00@RN0k(P`*S&XPh9ByjtohxN-%a*LCH`}^C^AW{r3;yNWVOK{R^6GPXA}_2Elth+zt?|`O;egBwuur;d z|6*}%rVL#KgMDK#dJ$zCb1qg@V8;$X%|gm47c`q>lofh|FYp8nLgdjGOtfkcF-dOl z-W{p`snd~j@zJ_onUzx}>Ttt4-*=bov4_T&+I7cErHF>L%k5aRH1_YU_u0w~e|kdiwBnj7y8 zvn%hizWvcboy+oFK~+VRGp1SqYCB0g3D;i2W{i-8VjzB`>LUb4ypjXMN4!u+G6aES z3@y=0)6NHurM1t^;n8p5D$-#hC!w+*HMLmRmeIOrMLLl(lzR!|i)u-UA}$jJ37)2e zQ95%42_H2g{Qmq_?{JG!Xzb1OXR135D{Ze{Glgp3v9;0!?>^debkqKuv;J#XgROdl z1=0gu0w{~t&9oMBy&Qo9P7~^Aea_SQF-)HoB1Vw(-AL@b+maK}OeaLep>Q6#I|p^O zzCVTEGk3*ET*6as)&KtO|G@Hqz;!@CwNj66&BINwUK|6{P;iBClkJm40WqX!)XITI zNX%s1HaiaO?3en~B713W^bX)vUY!coTtg9olQuku-+p3AXf zBI2|K=3FCI2vUlmyKvl&OlycPNj+5>85GZ*23$F?X#X#!8La(K>DWh2?p}$Y`BNr| z2Qp9x$*LeElUAh`L}jiN(rY}9P>K?^CD{&eFzBmD+xYnz0RqdgLaSCE{YndX)xB%C zf4pddu!RNAe6Bu6QW&(w*O!zo*JxMxF~CLQgp8@A44Dr~8a=wff)HH3{Tz!b;hBI@RXco;{A9xJ?;LjyC%(E!=^%-+Sk{S~jMG)VWH&2mGC zlJ;@Nwm-8Vor!o3x)<9MoA%;LZ&3>9NYhwCQd*hv)@Q+)`k$kF5K6)EVKhIpFzT+K zHN~ev`;qHWo~(q@hqy`ei1Vv-^aP7!JrAA`T;Z>wi&)A(Kx`^ru6H|+@oqoXYW~T4 z;pJIYaew#YsORz1Lc(W=YkgL48tr^mY6Nr|!WiJhfyq%m}=!`DkIb+qeqE0Ovc!mEI@dC`@Q|3J6@$%YOuM#Rw-cxB8*e#MLZ$0yUHXMUGrKaf31FnVKnm4Ic33;vf*yBdyjuV}yOY_sg*S%%b> zOr&F&HLV60w0jfZOcgdpfH$t()>eWj4nCVL_w~-Mo%{0v4m3e4X;oR((LP-n20EPR z5$6NlQ)}a-D!AhBWZ4G!GD#ekz>#ph4L6~JlB?26uzz-V!NEKUdB>@(2Zo_3)e3#)h518@Z%cSz8v57xTW0gQOqT?dHCpOk$`!IOA=_D_i-?Di& zafO5%=>ViFdfx3cGVj+YtF?``Ifpg;S~KNR?6eRV^Ni?O7!oxGN>6k2Cg=R3ndD4) zmMkNB4+{rXy+H;&(5O92q8BQ>23Nt)AVhdhL}YAjE;)0Z1~0+LG6Dt!1%(LhbIppU z32PA!`9X&r3^h}6G1QeGu^;y|oz&hxU`hX_qJ4hY`#;QaSK>)5pS#~7(3osuRXVpK zT($WtO4dG=JH{e(K&dw*;^S+l?p@=%Fxv7L`w^cb6j#BNS%+JtC&N9DK#}*->1d1A z$o#iYL}LaJicA*wb*86%z>a=qFFPZSYLAhD|K|HG){#R`O`OHh>jzo}8)O~Pih(MN zJfs^5d^XQPAKxnppsWC*<6Et<0VIXK4V(`!JB;gQ(Y}2hus{3Djc@?DyP1^?Uv(2z z(iT>Wex@?kgz2QPEaQOiEO6kKn$v*0-zN$GVmBi$q#8P%@xg{2T2}nEr92mLUwDn~ zC-|K>h;(oz*5?#fTUeE9D!-x!tyQpw8yf|7HK0Rn><6<#&SElqGhk12##J zjD3%y{S$>NdE%FTXBdWkPYWNr+KneU`k4@8Y7;i4an5VyY&s1B({v-b*`H@iM>PU~ zM(aIj3sTs6@6#6BN)B&J>{@?Z?!a-Kp9D_@PLSCGF zI~%=S&-eI8LE+z-%bG44e(0KIT#sM1b%Ktp;y99Lf{T(&BR%(eRXn@{d*wH(fSem~k`sY;XZeS-qox&pMh1#zN+ISxY0 zsVHX!cJ?T06o6zh+Tk5W1|9BuQ%=?99%R6!t+nu|ng4O<06v5$h5$aSUD+_>_zkyH zlTQkA5Fpn(9|M27iz^`i`(U?1&KxW1D-9+Gn_aQF&h`?Vom%*dj_tX_kd+NS-;72M zz5h)9ZV|$OB$?}1@Sl4VxZBFX{R*GAI7?ne_X~nx%r*AnC3IFg4Tcsm51?Sbn}X|? z7A_7Y{OykcsJ3H0FhZ2^2Ux{eeBzf+xos8X_GIO+W5!pnSt)>DD;}a{d)6(7#jA6r`J+vh-O=Crn!>aPy)}sN^oeO`vj#Qyb~eYrIZQpr0=VRyCFPuT zzY7VsGQNdzeZYnXc#*LJO}6&JDOtnH+PA5@r??8JR*hCj}?+fOyHzR3tuStBWr(5mux_z)`poI!)(8X&e%t0xF2c^S;pd>_Z>p?G4<+6iz5IvG zQGN!WY5FB2(Vdjk?T4NVQJ!afm^Fk=xd@k*#$N2xm^7N!6tZ{W1$+nJYagT{`uKgq z{OcJa2(mt0wrXN_$eVrOPW@MC#Y?jyITnLA7M>_{bza*=br+^nV6t*uTcDDv86=hR zj8l4wy_0)3fy>A=4NNk>_3`RJG@S<9skvpZJxlpWJKC8M4w7J})g9Xb^7}QJm5QXw8T_RTG#Ww)O4Hvh%=5Ly z8%4o?&JcuC8J|%hO_2jI?P>j-hn10*ey^q1S({DmBR!9Yf>n|_a>vZw&1`*!zum6$ zFcB<2>@{KI=s|A^2TDY7ar++7^UG&`rT2!7MQSv_zsy*Vv4ll~wG?;|^VGlM1AjAG zgfo=)llJg&Gq=n!h3{32tFR%rq*s~1^sOOo-P@K(tDm70S>$mAV()TuZU99 zK0)UYzmz)U{JXpP$0+%)T)}4X`I#uNGlrd-Uz4m;>+@_3k;rU?&`Mxd8j(WewyXPo z2#fGb4K*4?J=xm-n(5O+n&#VtykZrQE9C-77f zoy)}#j-szvB^X&aR{-P4@gapb66MhqNbwxsHrnYd+_Y?7>Mc~IQR_>v(t~apq3he@ zu>jc!DfjvQ(pv~bNKRH#Ox@f4YF(Mt`@kYchd%=zyhngHT7TOoWbOcLjLm{bWj0)P zA}1bhz2)$06^)93_rWCS>U{fxH9>OgBh58a_ z6}IRDIqO6uN`soV#4B&7W&~v76WHdfgJx3c0+EeUYi6yq{u(Uw>;Bis1@weBk**WP z61@tF=#9Q?h=j`Qe3RuQ^Dd6RS34lm8^!A*zg;!aa5-aAcEWqii3$0vCc#Q^8|&!< z9?qa;bm4$)VO1FH@mLVI0}&CAu0WTH{?|ZfOkkwPu^}(2m_NqE>)9T3dSs*?38yW0NKNdYNJ(mw{5A1tj zfG32w>H!wP3&Z9=5QDIh6;M>fIW4Cdb8EWC=ZzdJl%78oY50hYy59L~FJ|D7Aw?L& z7l}q~v3EgnU(l@Tk=T|owebB}-6`UC&2a5umul)Dj4x5Yi<909tI-xtT^7ifsDRlq zvoDfw=IGCfiJjPYNK(0iLfiL!FrEY6?*kF^$=^s1ZE@zT+b*0}X0#b>!G~3Ywfhc6ThR@jF z0#P~$zMaoesBX-5>BmE&#JEL@6T~fpyZIT2Isk+ID|jq!uRbwrx}Ov>4~!}hpaK=x zub495On;@#wvv1(W^Zg#Lf_(iqSAAn>hQ!O?lAJjESjp4#nKbjmd|>E{IW}lY(CG` z0`9iWtDWS#Q`g*)Cf)bH=*K8Vju%4w(|a6@C)8Sd_`?(w#Ir+#HAPe z*U=vXJ!Kbyi&LY`p_*d7LuT6g*zCi%aG$GdMHQP}4x0=VP-uu2eD7Hb(I^8`Z*qfcdLBpSA|HqPyuk)r>U15Pwy?e9Bos%_$mlt%3EzBs@$t|; zYY#&XcEs0WP+~iV|23i+H$1?pZFWgmRF^lZc10H@h!+@9bz@%mFquFRtA6wxhSuG6 z`?kfS4TIWYK69sKX?QSu+rB`jmD64PRa_!u)`}Wr07eRIdjuusjy=FeJYh|NfGso& z58;%1kBK+ltIy6&XbJj50mVl{=M7>2+~?lNzAYgS!8qK=qsxByUmP>9a_>b2LwrLj zuC;!%9h4bahmA+NwDqpZRM4?ge|)vyizi`3Y}02{O{?-SDFF`o|Cp*krXOTszlpiD zi4C0HsUxH-%Ur)D%f1>%{X(7BBEV6PA3w5rl76#{82Xmbc2Pn7kf_P5AvUCQ>1 ziybY-UB|97!3q|%?? z|1OtjBbxl)6eo~nl_KggpWs*4`7kD4S=fxoa-Qrny>vCtjVDG!ouVT_*)K6g+4s^fhh zo-_8?%46*q&h!Nrl6>>w{?uHZPj z|U!zF{N;xUwS6L9koq6etmh;eY5Wh&FnJ=itB78sbaF2A=qb z0}Urv1ld-g*Q%jjE@CU_;Os1p%HJatk5G&0cI+1!enoyI7qx#3_H6ZrezaA42RUE6 zIek1y@Td^2g;d;(u*_|xOpXT|S5C=VLec%-+$r!9WznZOU_bwmu_nWO`U7^;R=?p% zr-(JpigBt*ABt85LsR6N@0aU}A`C~dAgm;aArhMIgi=7S%tL)4*dKm=Ht*MUJJPwm zMmo9z!Hv%48<1y7-bSYk(Y=iiNB4jG2=ahr(cDVX_psdE{G>5qB)ci-<+3sg+9bkWH~m;oVhODaR6u9}}}t zWhljc?0QlgB0-G6#I5Fg1Yii6&Kv4X$$dIxU#~xg`3|8zA8*(YZ?|$?wTpACpN@&%}+@wfqFd@8y=qi>sS^;@kpHpdST$kt>=`E zKw3P2vUtomm}Sd8a!?}xy4$`wBcU8)#3ni;$hi=$V^A&OOyMcNOWeHtseM>je>=j4 zEku41eKg`W(J<^7G!{*;5p!PI_dAwa!=g5q=XJ6E|9AtOkq{ouX><;3*h;v6Vrwos zFeEp~&I{Z{wjkaxMf@j?0_;^*T!4(^6WTe)i|UQkPzJ&n=O35}>AfN@HFd@(zWn@EYNhFl5R&F&R( zV24l9Mvf2M402n&=2VmzMx%bA#G_*UAn<{GTH=3oKe5srN!7|%3uBo{BI)wdb)uC@ zS^-+b3hS|yShhd0DFhnFqUCU7(YZtT#@i`EAU)F4=om&U);er^Gpxy{T+5EYw7}DH z=K)-eBd4d&%KT6n%9u-hkBk#e8H2mO=`$nIhNEcrF|{*+QggZ`HA9EfpXb$H&>oS0pxlj@6yh$|1*vzXTbai@;RypT%Rmw@PZ5nF1buA^K(; zZx}6iMrB*0;GD2%KSh8tEc{c*WF8M&j7R;CG2EZwRY?+=FNfj61P1Y3^>kwg+dR?z z+4}N6)2Et--3K5AL|ql>Gg7mh$yGSJ=A4=@SiO06IuHq3QrX{@ylqs`dD|5a-wv0K zOPr1qAhFC_sq{!DsuqzD8SrY-{IAf^wPVUg6vl(4;{Sxo^_U0^LfPlzsz|*kc<+2q zSkY6`;+J~aOom4Ii(J?v=ZAKFsb?Y6dwHq<=STz`z}S|Q%a3@u_2-v13C^U|zUMDQ zs0wIP#M4AA8JkD-l+by9#=g)kKKUheWo^k|i`zxM_XjT-4uT>U1f_f!(ZPo_q4}cu zqvqu)VUTr5apeX{FAfEFc|PCV8!=9NK`7m)Yc3Hck^N`#!qJ5%=%cqb4cmMtPv)3w z%Q%;c82y87LHRm&2@x&x)AUc{!kjM~R~)Akdt>e%MzjGm!)8y|+6^YC=WjR+bk=^J z8#KajmT)wv*~$*#D%6rXKY#3d)DKSvc(s%q95!$nuVSnyn0~>~%86PH+v>&Mc|P~v z(uva!8A7Hf;_pY0AVrlCE&42X#w77&(mY#;fEE*aUkUr}i?jQdN>$1;F&9{bMi|xh zTR}YXR`a0>}3JxT6X*YWt_?rV^G;q&?nY1w*MR@-f<OsA_ z6B?STJ229lkUlCN`C0cE+9y6b_M<#xwCJE-pYy%|-8-@mpn-?k#&cOQuzpFL<77=E zV1=$Q_BU<%*9!$PL;S{WdcXQ46$Y}e4P&zXru^xany|pPV<~m1$&kDXeK8nbcdB|% z8%(gSIflL^*L5s%R!t8lWt@4k)EKmfh!K+Rsr!^DuYx8iA||dKOHa>mLAxhc5Eq-6 z3e(i*x4$|AmOsr^Rxkcl9xE9a0sFi~YDhmfw_c1nW1x_z3lm^Lz%>dcC{7mx51=LDM7jU zPDW_Z4^9mpTm70cBL|m-m<^sKNOh*FS_RvfHGhRfAv`cK;h3ksm1CrtWKac#Eb|Qh z3G7LKs%{Te`=1`S5|dDTL|24&GYEgD_#jkc`9C_~1jRFty$lLmRHA_%pIomy1v7ZZ zPnLbyTglUjfz`Ll2<|i)n;9xbx=o_<{iOZCn6mI(tOXh8#OGF`GMM?M4|bb@wlA{5 zJ_x(8!jf7Yvv%ee`?m73!{wJk@K*;* zr0;m`8#jFqgyfd}<^*fE1zKU}cO>bXO*8o*ew{cF0aW#eVOJ%VS)w`HL_vG;Z;=|u zf?;8Fly}qxpe6*dR<;wLlD>Js=HM<^v@`h+}*5YlC|Qo3qUvE##R* zNfe$z=J(&oM8zU#FMizb#s+hU$aPw4>Z=#{%PId4jrD<$*3plNADzTYqSwQkqVyVa z2lvX->|`wrCja9&HZ^uGhn$*g3R*Rm;U|Jr8mfLs2VcKtPE6N6BFZmI?ne}&&#+sA z%k^F+kc-@*3671n+Q2*^Tc2?v4%I*S7sn@46l|ohjLq4A9I7eXPot!La+Gvx2*LPB z!ZdY$D4NSMux}!zfhZ0ixO(&rU{Ub~1xBT5_(spO;ezB;hr3cd@~{}y4n&3wyck^X zBKGe4f4>bDq5l+`6207^kb`_x=G#ZW1qmimaJ?E0;tvx?+$o?PB!AQpN@C~nljo%7 z=#eM+OdzHsS19oT_LT1Nc6@MG2S4>`GU$9`%SyfNGw&^#_ruekY|~CW@Fj6#TS9{Z zA;I$+mw9K}HphK`72u?sqM70MJ1M3nQBD;ei$DN0W*_WnBIKQPa_H@R=4*<2Fb@H@ixJB ze~zyfyi%A@7mG7p&2q^%9~Gqr>Pa_l`WocvNS8i68QR-CUpr}`Y7Wu%hZ zk&S#hRx>`oHg7ee$x=KDQZjVxXT^jr886{Na9sL6YGvHI90_DI{Ek;|gw0*jeAP&H zIfGVL?#3@0qkfkey&M^Bq@cJ1=I(j0NDKrgfJUrhEBk;D zs&*zbB;p`$AzGwf1|rM8&H%9`z&%V0H(?4?mwGz#dllcnvX?sfmvQ1h8N1KWKs*7F zzyLvflb>C4R7{0p>8qj$8*1&8G1XIHmcicebQ^kkl!RYntr}t5<|whGTre+76vdAH zLfp}~aIO#Kq^=%mhL1trdAnqe){Gyl#T;H#YM;i;C?b-{wg?H?{Ye6E_&0<(+qxHT zc~;{ud_VYm{F?_!p9K6#lIEj1ul9m(SaO<3rfs}LsL!KSf?o`w5E!YCL9e&QQYuc6 z&z~KV(#pMy7ROwZe=Zf};&N5zAG26Dhh2o8tk@HZg1tIun}|h-t>9xq%+;k;p#piZ zgQG_{0Va-0hh9>H@P#&TZ?xG#jEhyXt+aBaN{SOAv~phcQ0pc*9b{2aP~O4^!d*)F z7@D2yqxfv_qGMF5$zbX`cA^Ee7vV9?2Pem*<=KkE52j36iP%o``UV%Od%r&O0Y~yr zJX@K~^7A<3u)&WS{Qw!M&0{T@DG@_;P33*dTf&C1!nAB zM`e?hESW%9AAP8WvO7uH>+9GTT1hAR6B`1nUUi6F1NI^Kbt7f>AElo{B6WfnM;Qq< zy%%h}-Y8oyjkrNhNv|)MO*iU&b|Y zo8Wz~?0fr9M*I9N+4O%OrUp-#aH0z4(|=wjU~tK3R0`k1IgHnx6s;$SYCsvk#`eoz z^M0qy86&@+z}XkLZHe$O!MPcRpinA==`$wHBo!r1AKc;sAW8j-7t9nIw3GiB(h;9H z{RI7#E87a2t`V31!tA8{OQtzxG#a+3QdL?OALgRk#4nv-zxXZmZo(fj9P@Ko$P6#= zIbO^cy~`5+KQkU60%0o}dzr6LGG)zcFRB?N-&1IAvN)OX?w@>Y2x{f9;1Tn^CO+2Y zVU~V5?6XKiVUi{t*q&B4mHF*i5P+X`1@Evw?)nuPj184MPHG;}EX&A2qVD04e=!du zc18g_*LYvbsk9begF@&u+4yl+{WS_6mz7$J%n6*$=G!N*ee;pMtoP zb?jc6rWe-2Tln2+VnAk{e)`{0CV~N4?ly1dP zi-zy)?M<`POjGx=ZW?jIp)jJgXl_W}m!?{i$$>>PY1<)#N_dR>r&jm(ze}C99iK?Y zWmG-IySY2wza9ILugguSS5YWO^=@t7HVFr}4<=KXd4JVBdU)BZEhdaP~h&a zaOnNjRp$NOvG3hYE5f-X*-%H|pGR7PT0% zlYaTO*~sykH_q9}Z}md_PG0mG7uM?SY#SXo=TC&~-1w2KaH|TL;#bc62zja!D9T9f z8bImIPLr7myZx2fTaaRGxRsA2YO+ zx*uQvF-t#8b*I1hv{j-Q!F3jPL81R*$Y0X@^YS2K{-3qPs>gG_YzTBwb;g^Q4#7He zF`xAk7PBQ4J35AJX_A?#eC6^>8A>Kfr+4cI2Z%3+&yCq__)fXQPO2&%{v;@G@xp!@ z%eSt7kV7L5ZQIvZnOkSPdf%bSC;9YP&sgWT+qO9Cr}Xx!^-$$W)fpKq5iteIxFZPc zef@{EYI&}$OzI&WiWUW8K_)X^7e=v9q>ft&uiEKq# z&jfj@6CxKJSA}Bs6sX)Udl>W7UY~v;3X-<))Y*8M@(E)t#lW4z>(T9vr&OeaBBf`O zgEn&&{qNml>T0ql%lHL{+hq*g+}yPz{0^g;l@$qgk-R@`%3?JG&|iG8>sBd5PBb~~ zjH;NI!VPVTGZrk`|ALzl&M{P9I>Lksx8K#N-qbutrl0hjxObS@-N@Lp%3q+xy64Pm zqW0k1T}2o~m{K=j6}hQ&`z3`PP|2i1p0X|ats+xUoLA@Sj)XXop7}aAsmC-k?zgJ+ zO`2m{@M5nDsM$R}u5`St--8+{gSgoLS1BObnn2GC88Jjq=i zSfA^<3^N&f)8oVwq;NsKi3xVn5XdnPyq!%6vq}PDa=vZpCE+ z-gx@+lSZheidN;P6Q@?C$rVQBAMu$ujGOYjj}hPe19;|3!kywcjXJ9D83*b}(8ZXU%_?M@nevyiZwe<$qidz8%ayoNKrlJ_x#m)WkpI6VK2i;$vcMVfu zy58I_OivgH=`92hhFH|K&XlyyG^pw&>D2r0Icgc3p3!Ud=igdsYja<~1_bJl?FJb; z2YQEE;=`l9#gSS4X1hW{hB>{Nekq=u@by@_yAWCHD%U37TYCQ%H~lPAtad_U@2%D5 zK^x-V+XHx$@3iaAFXL(G{(fEHuOae@7UvAR`vd0Qp!+GyVOPVf;cwgsL`vCJiV7P& ztNeplBCH+hNQ!`BmE-xs!^$@-p*GhdiscqR#Rdlg7BrIwqjh(&_Qehs+$+1K%&eoi zXc=2{_$TR0xM-cI_Yme{F_bM$5us}IwMjfn?Wx)bC6R}F)w#u`y`gWMo^ph~P5+6T z-+UwN|5{=s(vFHtbd?!zbK57q0Im@3nMk1Ez>GaQ(k5vD;T}~oXoMkOXiWOZZoiL(S)+E?3U%Hoo>{HeHcM(X zE%SEt4EqHiRn5ECqDNul4=%8#>;Q$b5vlFm{TE&1!05RuV6cFU*|e>dPLa2CNwyI#LJJOy6sL$Z&K` zsOB@R0^OqIa}0#FA9N$<6_wnVEWe*g?iCGOMb?w?eb(<(x|ASW8pofwWQtl{^7S~* z{s(i|yGPtNg4lY@+@+5Ki!oL+T3GY_6`_3tVTuKXTEy)0w&Wwj^4jlb6TfB5gs;S}3_39Cpm3ZeCzLlh=Pd0Mz zF^21he=lEn@R(L`9d$DqzX)u38?m!3Eo~aQGa_MG((B^^FaEMd4=cnis;xB3OMkY8 z(()n}52JHX;TRjHD>*S4)iKa8*pN=(-*=oUC?xiMm*`@*_(H&c5|Uzty)GI^?{vd1 zOjTQj0eP+I+5&< zzCrnPX<1WwaDWbhI!JT#&qd>8lT`j~>U|$ee%xv`mJ{-_>;2ELv!LALq-Xc^%g2Um zCvu&F8d8|{3gSI$CuJ!aqLV8hOCR~zHz^fwlkd}F=hYU)83 zm|dzYXi9|#Ks;TeX=UEniA$ZWs8~{rNu8glcos}(^F?#yDB+Ij4_=M_ zy*8Z_H5`qJyL_|EJ~_}jI1_>X9wUFKV21y2+M)i136}{q?CmjMZntf}xu_Jv(zx$2 zH(3O0SWTw^Qvubz{Eg2>7Wu9N%tX{twLzI)=6Rz|qddV*$ z&WtNrRhy3Y2?j@rK;D2#%v9S{Uj64(6jdBK2m^(d&bx&8H!jA+77mE=VgyA^vGN+{Y z_SwvyLo_~wY&`jU$GK>k|EM+kly0;9LaI;Z4%1}QEe?VOAzR=7>*Bn_*^JshZWS%5 zRf?#ZwRhDX6`LBhYtvRGV$~i|MNmbI(As-7_HNZyq*bMe8VxF?)SeOJ7k%INpWpc> z*Ofn>>p9Ok=RWu6`#tBjH+zaNJ?qWux`7^=6N*hXnI|y5kWeqL=gzJJq zX0cDckjFoWq4?YT{>Y6I(C@d=>Z=2bYRR_L&UOCkgj=g^@@JZ00v0s@pNeFjVJ_6= zm(!ZgEb!uLWYwqT>tD&@e0Z1MUw!m3*Fi!^k;}$x@;xwO%~HPSWhe1|YPLU zm1?^uSpyqsTZgC0*c&-@RCf3KtC|46+Kb@z;S9aJw;K}58G>X0bE<4hzxU2VNMGhU zl0=!l@&S;xkruqJE>4Q>2}j`5kC5dWAUH167YO(Zi0vj{#K~~RhBiva zvQlDvRPPoqiCvP*%kNJMNXY?h-*HB{Io$7xC~5CH6_9dA>D6M-=MRtyg$qB6u+=>s z;?3yqMcCLbXb1p5kd|46zzR!s#sNX zeL0H#B0jQ)z6Q*%eX~H+;J>Y4_j#ky|GvLtGJE<%*vU~CT~!)}SRzkk^I(;Yc1|0- zFc#o=!T-V6wln0U-#6fz;j#YG{j;&x4ZipKR`t$B<#tIXx-ITYitD?WH_EuFxIoLE z6j5pd1l8o$HHlG^h&9A*ryzazlaC3f-&NFyw%I`Y@U{wW0|%UG`U=Oa{{8_Z^y^%m z`|Q@6M;{HpwSdyvFz8L$nBgy(eH`bf-0vJ>N@QgrV!v3Ra%wDCt53ljDhxoN6>DiZ z`H3YFh=QiqTP@tiJ8z3O-W!;bT$HYAHNG{$^&*!WrM9d_$0f%{A#)S&Jyid>`ZI-1 z$z9Uz)K7FLDp%&CqwWmx6tkY6(y zjS(;Pj&D)irt$9b$CSlj4$`}b8My3?FFzGeJ_UU(?E<_}ditES6(4K=)sw zVm$HU^E$C&VkJNq+U*_nrxxp}^k*O^?LnXogziU3dTvIKPeMzi(iO9h!e7xrB<^z) z6F%LMl3#OY8LB>gY#sC*Z9h4DnJGL$2LxtfK*X`&X(lGRN)BCrhAN_4)hD~WOxxD= z>O!1~OV+{g#?v4!H%o{G&$NMcm;bxochEg&+Lze@7xDJR8;{Nmdd94tJkdi$a2Z8L z3@fHC-Sib{U6ZJKh~>A07r!d+M~BYRa$9PnT6AnUQb*mW*g6gqtf536k z;<0>dGjuws6@?P!aZ);Q_^^qQFafL#Tf*4I;(}jVFw5sSeMg1ao2koifTSOnBQ2Oj zHDz+s0#M(l-ZLa4WAhp>>Lo>2fV^YV#KsEg{`Bea_|WXm3s9BV?y7SK_PutIB^#Y1 zXCFt&knm6Cq#boA#39B{q&BIAH|eLbP{eCwVM{|2MiMAgTir0kk>*hj2sKGXq~ zb-)C-BakexQM^s*^FviOD_cMA`YnH@t3^#orilo8{~B_+>YB0O!h}%c^bhtQTWa(C zoBc{wIr{0PcCYS`h0+xJ$$w*C&RbU%oa+v&0TK9zI^P*~io;bKc{{aB1@3aqnnIT4 zsAP2;)=*K8l=Xw)M5-=0<%1NTwni5Hdrdj=b-K8x)A~UQa&C9R*0Hg*faRZ!NDaw; z{h+4sFEf)3Cv*fTu{Pc#w#XErkJwiVH?mU)!M5V~(|EBh(|}j-djaY|O|G}IdPr$_ z(Y0o1-t_NCj%NEj)@K>4Zae3k|G4#EcbMH3v?G=vWOmW57#=PK%jON=y(=9-Y^hBu zUpl7_m6W%x*s=vwX$9Z5HF+4gr)S+Hw&j!1X>^+<3Sl5FKO{7{U?<_4qyiCV2KwqmX z4VJvB#YZf>49?CIPK8g-B>di1^b{L~53#SsN}9Sh1ou&vr90X`aPm3Zn>eGz_n3*` z1i`c}=d>4sOdo?&o7Bo~+6<@zr`TBF>u9-)T z#3k%2a;?Qy^ejnLy7+3!47`6w*&~%=jd)>?6968@rAj^S5FBg}>I{&@y;;~=tb_?< zk;LUVZRFFyW%`u2`!ix3#7S%9#;Vh>%+<-HPz-NKu$FV*pttIb^!_xXUoo0vlwTrSz2jlPI%=3Nbcpgt7bmD^0jjB zT7UzVcelnAM_HO90~BZo%@}m$QNlxX3yIt|lgYnecPT_pP?p>);B{&1x#6pcBaC(B zI=I(!2gY;azT%L|Qy=jZ3_wm=zVdRmXA|_zC zm8V1|zD&a|ckJ|%xF{uyXdgr)ABEy9BLUW?VAtF*Gg1*bhn^Q;x;2J{F`V_nQx8>Q z^BGAsjE?E=5(c~&sVpl^ckj_J*U0ySKVV23gfswY?)xHBCT;kHZeBUuN=o*wtrwP> ztz?bL(1I6i^2LIASqS@drcMgTqbYe!CBRdouA9I-*HaQ@WACq0*Mj4$ z9Mum}R^BgPxV zgPAY%sBAqns4F^L9e3A66rwM`Y!sq#Jg8Bzv6zWFi(O4V*~#`K!r-Oitn4qN5?Mj| zor(3CH+DS&bySpc^(TU3G^YtH*u!Vr=PM;#TAKAJZYKKKESJiH0HhoKIGIv*FfXaG zdNxf{OIrH{J=C`It@lG%LFaf;r+S^V&>9;*Vkce;)sh{_=!yravUn1Yh+l6wKI>D) zvpu`y3=`Nt8tsg$xDS+<=a1>U@DU&6=xuzw7iib{ddEk5b7s%aACI|k`MM2yQ^eJO zr_VUh+xjhq&m{6*{N4f6vH+1+ZvTa~h5n-zA~{7794}-9t^LK=)tIEWbEtBT?h+#i zu|jU;ClnZ71tE%^hs}&VCzcMAEae)Mks!+f=0~W%ZMY2D;NH9=amGPbk?h|q z_K>Rl*yi20PA^ZRX$94|$^XM)-)=HUOO^>w6jd+;OwS%Q^!P8O`U6JGS_W<)9&tDL zCjgV@3raZuTnybVgW?2F&z^?9U%M$Yjy9mSMP>Cd5YX z957SiJ=MF?G*WfQXY)5S0H+}eyYn9wyJRw_XxeTw;qWmGz9`?p!L+bt;y=*EhyAOqGH)|khY4Ne?ydyXh1egpZ*u=2gOmw4LH{`})Qwxlyuy=K zjT<3pa6Kl5`U$)0eTrd(g}u2->Y|3~t}Sq~uS@ zF8S?6B&DCGpJoWbv$>}`^un%`N2a50Z@Sa*A})L)#fNL?BT7|~ABT97%uk`;ajRbc ztZ3BhiNztz<5+z?*Ro32{vdMEC758)-*0(0_Ec4EWX4lf=-m)_^QNZ=-FtZHe)kT4 zcxGI|LT*ZgdRz;@IKZ46Z%zB1$s;fA|BnfSto(E!$d$=1Ic3eOx@y*QqJnr=?65aJ zXz?(LFUMfoD{;+Gr%Vu=9Z>Pxh_3bAU2<6rH+5z;d#ik{)%P8e#n;XyI04->D(}ssy^98xt(yf8u6+W>&Jvla1QH4iLFu1-QLl$0dE+qG}hxIwaTp)>IaY9S9| z^HcH5y_}EJ8z17Ax<~47QKV=Y zXx2R4mMx@8As1Q5S&%`$gZ|*$;#<1TaF3g=UCxlB5s9TXGtOxUGz@D;LUx`YG}|7R z2nP+_qT?nPWU%BEbDJ_*@K?Fio);+xnj@_x=SZTJR%7DWCnCvNL3*Li-RK#s-DlIn z$yr^0GlU_1qoD~i4@zJC`25@;VzNasWJ#(b@&{VD(|BV;F|V90S8~jpA(0DDdgxXY z3g)_T<5Byo8S1_E<nMidS0CR#Dwi!&>HG@}(;dwBl9U**{Ii9dY6WVTvsMY%Mn z$SBMbu4bj-iK}Rwt3^WN3Bn?`ts)yk68g~=>=8PC%&(bjr)v5Abyu34100`1m2wYn zYn>5z%|m3Y-wwB54t2>rC*Di#;gb)dxDL_&#Ho)_yYT4kPhLUnTF}$+RHm}dSRJ3M zvBj*io9&CQ-`2PQS(Bl*8Oy)D8FTrl7jE2@|7$rF6+t?9okGD8hoO-=cxbLlU->j{hfoqPsY!5 zK2mV}BZ;M_gt%_@8wH|gw)5X69qgezbh}1db`v3MWyvu=&>do~|837ns8YLO6wqUH zB#l|1T_MB3#|=pAu-%x%!$puGh-^zyTXEe--OK{E8?SC2=0{1O%=31VW+xku6X;$# zL)csU>9^hIRM2Qn)$l_;5QXJaJKQyeIqWn4mt6KW_BDDA=Xo66rUl7&j)MA@e+u%S zGiblD@<7ZLL)|_FX+M!&F)*)99HGm!4Q6q+EkSzihggWXS)I#tjr`Frx<)_e|2p~B zZWt76us%dIsPBYnD-dZ6S^G*}9@^FPc;WfC2~}k&1St=-I;=8CSO&iAJ@;|}vEU7F zQD7~%feROB_oCf{2@zTvVGBpLWM63oSxgK6B3;swYyi}ALu&ZFX*RwF{BOEDwL(xo zi$Qk~lelx^?#WU7C2TqdjU8RB=t1vL3g$CHv<4V|>-&swCOm zfRy-*e(zTmaW?5Wk2s?03_6T$%dXCmBW<$Rk@~U~`Xlhl$`jpi^s}EZ_0i1~bZ}*^ zldg90p^KxLh7o$r)xskGTzgsDDdlux>~7eiBG{B|o@%RWhna9S@4b*3aiwbkSnGS^ z=vsCG3{+?Km42CCC%g9XjLS#5lLGIu|E{;Rr4A;gC*Kt}vM7ZP)ZF^T9ol4%66$Ai zVfw4{KvU|O6Nq)i?yQymfrki?%!2nne3j|tYtCg zMH@$G%6-+9j_yHJ$C&d&(0WgWd}c1HE_#1wWyF;S?H;BOV-Qh)s_3DYB{7Xk)+U~L zVD&W77s8cmOTsH3UVBT!FK0CK`3wI1Q7N>)hi#Hu$!jCWYH(CQKm8-1eks_qx|!)O zU&L|N$@8C}{YnezWoK^MaLjgF%7y|EkCHc*ot1VhcnuCx135097*O1rwFA!91d zr-DV4SZAyl*EX;?Hs67irhQ=8(vcol$vl7C~W2G{|M(2Va3^w)3W{qAO}so z;fQ%^YF7CUU4+S`y1ySi61!J1!Fcb-ZpZ8Mblr_3&4>kMlaT7y9lRsf=SB@L5E%Mw z4PdYAWY&2#?oWgC62?5?q+~^V!SLa#%A7unVu&Mgpn z0ceFKb^k0aj}ED_6(ueYJ@dDVr{&W3DVpskjiA}pVFsE$s9&>zSHMpB)70KniJ72O z=jG;xtqB!cq>e^4;*lt0D(baIxx`f0E1b_vO=}c81x$3;SL*T<%*cr1(dy;)aj6+^ z+uV;)+qoI8jwazL_nH9!CHHg@@>ZvGVV=&7&jB&V@rma4q(3aRyLA-{cb}`u&XkI_ z?)Dq=a~iQIjWai>u_TlWyin3YkFsAw6?$9YwCJUeSNADSc#0^stdn;78o}Nx>*V5X z>Wux#;wU&juGYe#pW}eSxbDv`Rc)mqUI6ukZ~Y=_oGFgoglGq90zBV!=uRv&k^^2G zbUHRFs>@hK&$NO<1i)1u1BSQYEQcZ-WJ7!#+f|U^y0L7`$pD{je~xZkxZR` z{U6C~g_DE3_bG^E^=-jcPFO9Pw(Z6==YsII)UNB!ffoLDRvyvVA=-f#gcbp!u3?Km zS z{J-FFrYj%ucVV47YL4WKaZ0|9=YKG1dq^LDbLPwm2aK!)+IdGAT|R?5O4a+{!?>e_ z91S^bg#Jraf@-+@KTLIh6T4skMX+@+&k^CRK;qFuwF|m;SR6C$P_R1jeh`VCmXT(S IhSSUc19Dpx!vFvP literal 0 HcmV?d00001 diff --git a/docs/translations/README.ar-SA.md b/docs/translations/README.ar-SA.md index b4b364a91..f3f4a78c2 100644 --- a/docs/translations/README.ar-SA.md +++ b/docs/translations/README.ar-SA.md @@ -11,7 +11,7 @@ PyPI Downloads Sponsor - LinkedIn + LinkedIn

diff --git a/docs/translations/README.de-DE.md b/docs/translations/README.de-DE.md index e3a293b7b..f81067902 100644 --- a/docs/translations/README.de-DE.md +++ b/docs/translations/README.de-DE.md @@ -11,7 +11,7 @@ PyPI Downloads Sponsor - LinkedIn + LinkedIn

**Eine KI-Coding-Assistent-Skill.** Tippe `/graphify` in Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro oder Google Antigravity — es liest deine Dateien, baut einen Wissensgraphen und gibt dir Struktur zurück, die du vorher nicht sehen konntest. Verstehe eine Codebasis schneller. Finde das „Warum" hinter Architekturentscheidungen. diff --git a/docs/translations/README.es-ES.md b/docs/translations/README.es-ES.md index 354c32ec0..7c58a2b27 100644 --- a/docs/translations/README.es-ES.md +++ b/docs/translations/README.es-ES.md @@ -11,7 +11,7 @@ PyPI Downloads Sponsor - LinkedIn + LinkedIn

**Una habilidad para asistentes de código IA.** Escribe `/graphify` en Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro o Google Antigravity — lee tus archivos, construye un grafo de conocimiento y te devuelve estructura que no sabías que existía. Entiende una base de código más rápido. Encuentra el «por qué» detrás de las decisiones arquitectónicas. diff --git a/docs/translations/README.fa-IR.md b/docs/translations/README.fa-IR.md index c52e6a22a..2851c0803 100644 --- a/docs/translations/README.fa-IR.md +++ b/docs/translations/README.fa-IR.md @@ -1,5 +1,5 @@

- Graphify + Graphify

@@ -12,7 +12,7 @@ PyPI Downloads Sponsor - LinkedIn + LinkedIn X

@@ -511,11 +511,11 @@ graphify --version ## ساخته‌شده روی graphify — Penpax -[**Penpax**](https://graphifylabs.ai) لایه همیشه‌روشن ساخته‌شده روی graphify است — همان رویکرد گراف را بر کل زندگی کاری شما اعمال می‌کند: جلسات، تاریخچه مرورگر، ایمیل‌ها، فایل‌ها و کد، به‌صورت مداوم در پس‌زمینه به‌روزرسانی می‌شود. +[**Penpax**](https://graphify.com) لایه همیشه‌روشن ساخته‌شده روی graphify است — همان رویکرد گراف را بر کل زندگی کاری شما اعمال می‌کند: جلسات، تاریخچه مرورگر، ایمیل‌ها، فایل‌ها و کد، به‌صورت مداوم در پس‌زمینه به‌روزرسانی می‌شود. ساخته‌شده برای کسانی که کارشان در صدها مکالمه و سند پراکنده است. بدون ابر، کاملاً روی دستگاه. -**آزمایش رایگان به‌زودی راه‌اندازی می‌شود.** [به لیست انتظار بپیوندید ←](https://graphifylabs.ai) +**آزمایش رایگان به‌زودی راه‌اندازی می‌شود.** [به لیست انتظار بپیوندید ←](https://graphify.com) --- diff --git a/docs/translations/README.fil-PH.md b/docs/translations/README.fil-PH.md index 6a8ef4cda..e318eb68e 100644 --- a/docs/translations/README.fil-PH.md +++ b/docs/translations/README.fil-PH.md @@ -72,6 +72,6 @@ Ang mga code file ay prinoseso nang lokal sa pamamagitan ng tree-sitter AST. Ang ## Binuo sa ibabaw ng graphify — Penpax -Ang [**Penpax**](https://graphifylabs.ai) ay ang enterprise layer sa ibabaw ng graphify. **Malapit nang magkaroon ng libreng trial.** [Sumali sa waitlist →](https://graphifylabs.ai) +Ang [**Penpax**](https://graphify.com) ay ang enterprise layer sa ibabaw ng graphify. **Malapit nang magkaroon ng libreng trial.** [Sumali sa waitlist →](https://graphify.com) [![Star History Chart](https://api.star-history.com/svg?repos=safishamsi/graphify&type=Date)](https://star-history.com/#safishamsi/graphify&Date) diff --git a/docs/translations/README.fr-FR.md b/docs/translations/README.fr-FR.md index 1a1e3f939..eb57cd638 100644 --- a/docs/translations/README.fr-FR.md +++ b/docs/translations/README.fr-FR.md @@ -11,7 +11,7 @@ PyPI Downloads Sponsor - LinkedIn + LinkedIn

**Une compétence pour assistant de code IA.** Tapez `/graphify` dans Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro ou Google Antigravity — il lit vos fichiers, construit un graphe de connaissances et vous révèle une structure que vous ne voyiez pas auparavant. Comprenez une base de code plus rapidement. Trouvez le « pourquoi » derrière les décisions architecturales. diff --git a/docs/translations/README.he-IL.md b/docs/translations/README.he-IL.md new file mode 100644 index 000000000..65e9ae75d --- /dev/null +++ b/docs/translations/README.he-IL.md @@ -0,0 +1,849 @@ +

+ Graphify +

+ +

+ 🇺🇸 English | 🇨🇳 简体中文 | 🇯🇵 日本語 | 🇰🇷 한국어 | 🇩🇪 Deutsch | 🇫🇷 Français | 🇪🇸 Español | 🇮🇳 हिन्दी | 🇧🇷 Português | 🇷🇺 Русский | 🇸🇦 العربية | 🇮🇷 فارسی | 🇮🇹 Italiano | 🇵🇱 Polski | 🇳🇱 Nederlands | 🇹🇷 Türkçe | 🇺🇦 Українська | 🇻🇳 Tiếng Việt | 🇮🇩 Bahasa Indonesia | 🇸🇪 Svenska | 🇬🇷 Ελληνικά | 🇷🇴 Română | 🇨🇿 Čeština | 🇫🇮 Suomi | 🇩🇰 Dansk | 🇳🇴 Norsk | 🇭🇺 Magyar | 🇹🇭 ภาษาไทย | 🇺🇿 Oʻzbekcha | 🇹🇼 繁體中文 | 🇵🇭 Filipino | 🇮🇱 עברית +

+ +

+ YC S26 + Discord + The Memory Layer + CI + PyPI + Downloads + Sponsor + LinkedIn + X +

+ +

+ + Star History Chart + +

+ +
+ +הקלידו `‎/graphify` בעוזר ה-AI לכתיבת קוד שלכם, והוא ימפה את הפרויקט כולו — קוד, מסמכים, קובצי PDF, תמונות, סרטונים — לגרף ידע שאפשר לשאול עליו שאלות, במקום לחפש בקבצים עם grep. + +עובד ב-Claude Code, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, Amp, OpenClaw, Factory Droid, Trae, Hermes, Kimi Code, Kiro, Pi, Devin CLI ו-Google Antigravity. + +
+ +``` +/graphify . +``` + +
+ +זה הכול. מקבלים שלושה קבצים: + +
+ +``` +graphify-out/ +├── graph.html נפתח בכל דפדפן — לחיצה על צמתים, סינון, חיפוש +├── GRAPH_REPORT.md עיקרי הדברים: מושגי מפתח, קשרים מפתיעים, שאלות מוצעות +└── graph.json הגרף המלא — אפשר לשאול עליו בכל רגע בלי לקרוא שוב את הקבצים +``` + +
+ +לדף ארכיטקטורה קריא עם דיאגרמות זרימת-קריאות ב-Mermaid, הריצו: + +
+ +```bash +graphify export callflow-html +``` + +--- + +
+ +## דרישות מקדימות + +| דרישה | מינימום | בדיקה | התקנה | +|---|---|---|---| +| Python | 3.10+ | `python --version` | [python.org](https://www.python.org/downloads/) | +| uv *(מומלץ)* | כל גרסה | `uv --version` | `curl -LsSf https://astral.sh/uv/install.sh \| sh` | +| pipx *(חלופה)* | כל גרסה | `pipx --version` | `pip install pipx` | + +**התקנה מהירה ב-macOS (עם Homebrew):** + +
+ +```bash +brew install python@3.12 uv +``` + +
+ +**התקנה מהירה ב-Windows:** + +
+ +```powershell +winget install astral-sh.uv +``` + +
+ +**Ubuntu/Debian:** + +
+ +```bash +sudo apt install python3.12 python3-pip pipx +# או התקנת uv: +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +--- + +
+ +## התקנה + +> **החבילה הרשמית:** חבילת ה-PyPI היא `graphifyy` (עם y כפולה). חבילות `graphify*` אחרות ב-PyPI אינן קשורות לפרויקט. פקודת ה-CLI היא עדיין `graphify`. + +**שלב 1 — התקנת החבילה:** + +
+ +```bash +# מומלץ (סביבה מבודדת; אם הפקודה 'graphify' לא נמצאת אחר כך, הריצו: uv tool update-shell): +uv tool install graphifyy + +# חלופות: +pipx install graphifyy +pip install graphifyy # עשוי לדרוש הגדרת PATH — ראו הערה בהמשך +``` + +
+ +**שלב 2 — רישום המיומנות (skill) אצל עוזר ה-AI שלכם:** + +
+ +```bash +graphify install +``` + +
+ +זהו. פתחו את עוזר ה-AI והקלידו `‎/graphify .` + +כדי להתקין את המיומנות בתוך המאגר הנוכחי במקום בפרופיל המשתמש, הוסיפו `--project`: + +
+ +```bash +graphify install --project +graphify install --project --platform codex +``` + +
+ +התקנה ברמת הפרויקט כותבת לתוך התיקייה הנוכחית, למשל `.claude/skills/graphify/SKILL.md` או `.agents/skills/graphify/SKILL.md` (בתוספת תיקיית `references/` שהמיומנות טוענת לפי הצורך), ומדפיסה רמז `git add` לקבצים שאפשר לבצע להם commit. פקודות פר-פלטפורמה שתומכות בהתקנה ברמת הפרויקט מקבלות את אותו דגל, למשל `graphify claude install --project` או `graphify codex install --project`. + +> **הערת PowerShell:** השתמשו ב-`graphify .` ולא ב-`‎/graphify .` — הלוכסן המוביל הוא מפריד נתיבים ב-PowerShell. + +> **‏`graphify: command not found`?** ‏`uv tool install` / `pipx install` מציבים את פקודת `graphify` בתיקיית הכלים שלהם (`~/.local/bin`). אם המעטפת (shell) לא מוצאת אותה מיד אחרי ההתקנה — נפוץ בהתקנת macOS + zsh טרייה — התיקייה הזו עדיין לא ב-`PATH`: הריצו `uv tool update-shell` (או `pipx ensurepath`) ופתחו טרמינל חדש. עם `pip` רגיל, הוסיפו את `~/.local/bin` (בלינוקס) או `~/Library/Python/3.x/bin` (במק) ל-PATH, או הריצו `python -m graphify`. + +> **מריצים עם `uvx` / `uv tool run` בלי להתקין?** ציינו את שם החבילה, לא את שם הפקודה: `uvx --from graphifyy graphify install`. ‏`uvx graphify …` רגיל נכשל (`No solution found … no versions of graphify`) כי `uv tool run` קורא את המילה הראשונה כשם *חבילה*, והחבילה היא `graphifyy` — פקודת `graphify` נמצאת בתוכה. + +> **הימנעו מ-`pip install` במק/Windows** אם אפשר. המיומנות מאתרת את Python בזמן ריצה מתוך `graphify-out/.graphify_python`; אם הוא מצביע על סביבה שונה מזו שבה `pip` התקין את החבילה, תקבלו `ModuleNotFoundError: No module named 'graphify'`. ‏`uv tool install` ו-`pipx install` מבודדים את החבילה בסביבה משלהם ונמנעים מהבעיה לחלוטין. + +> **הוקים של Git עם uv tool / pipx:** ‏`graphify hook install` מטמיע את נתיב המפרש הנוכחי ישירות בסקריפטי ההוק בזמן ההתקנה, כך שהוק ה-post-commit יופעל כראוי גם בלקוחות Git גרפיים וב-CI שבהם `~/.local/bin` אינו ב-PATH. אם התקנתם מחדש או שדרגתם את graphify, הריצו שוב `graphify hook install` כדי לרענן את הנתיב המוטמע. + +### בחרו את הפלטפורמה שלכם + +| פלטפורמה | פקודת התקנה | +|----------|----------------| +| Claude Code (לינוקס/מק) | `graphify install` | +| Claude Code (Windows) | `graphify install` (זיהוי אוטומטי) או `graphify install --platform windows` | +| CodeBuddy | `graphify install --platform codebuddy` | +| Codex | `graphify install --platform codex` | +| OpenCode | `graphify install --platform opencode` | +| Kilo Code | `graphify install --platform kilo` | +| GitHub Copilot CLI | `graphify install --platform copilot` | +| VS Code Copilot Chat | `graphify vscode install` | +| Aider | `graphify install --platform aider` | +| OpenClaw | `graphify install --platform claw` | +| Factory Droid | `graphify install --platform droid` | +| Trae | `graphify install --platform trae` | +| Trae CN | `graphify install --platform trae-cn` | +| Gemini CLI | `graphify install --platform gemini` | +| Hermes | `graphify install --platform hermes` | +| Kimi Code | `graphify install --platform kimi` | +| Amp | `graphify amp install` | +| Agent Skills (חוצה-פלטפורמות) | `graphify install --platform agents` (כינוי: `--platform skills`) | +| Kiro IDE/CLI | `graphify kiro install` | +| Pi coding agent | `graphify install --platform pi` | +| Cursor | `graphify cursor install` | +| Devin CLI | `graphify devin install` | +| Google Antigravity | `graphify antigravity install` | + +משתמשי Codex צריכים גם `multi_agent = true` תחת `[features]` בקובץ `~/.codex/config.toml` לצורך חילוץ מקבילי. CodeBuddy משתמש באותו מנגנון Agent tool והוק PreToolUse כמו Claude Code. ‏Factory Droid משתמש בכלי `Task` לשיגור תת-סוכנים במקביל. OpenClaw ו-Aider משתמשים בחילוץ טורי (תמיכה בסוכנים מקביליים עדיין מוקדמת בפלטפורמות אלו). Trae משתמש ב-Agent tool לשיגור תת-סוכנים במקביל ו**אינו** תומך בהוקים מסוג PreToolUse — ‏AGENTS.md הוא המנגנון הקבוע שם. + +`--platform agents` (כינוי: `--platform skills`) מכוון למיקומים הגנריים חוצי-הפלטפורמות של [Agent-Skills](https://github.com/anthropics/skills): ‏`~/.agents/skills/` הגלובלי של המשתמש (נקרא על ידי `npx skills` ומסגרות תואמות-מפרט) בהתקנה גלובלית, ו-`./.agents/skills/` בהתקנת פרויקט (`--project`). ‏`graphify install` החשוף נשאר חד-פלטפורמי (Claude Code) בכוונה — השתמשו בפלטפורמת `agents` כשתרצו שהמיומנות תהיה זמינה לכל מסגרת שקוראת `.agents/skills`. + +> Codex משתמש ב-`‎$graphify` במקום `‎/graphify`. + +### תוספים אופציונליים + +התקינו רק מה שצריך: + +| תוסף | מה הוא מוסיף | התקנה | +|---|---|---| +| `pdf` | חילוץ PDF | `uv tool install "graphifyy[pdf]"` | +| `office` | תמיכה ב-`.docx` ו-`.xlsx` | `uv tool install "graphifyy[office]"` | +| `google` | רינדור Google Sheets | `uv tool install "graphifyy[google]"` | +| `video` | תמלול וידאו/אודיו (faster-whisper + yt-dlp) | `uv tool install "graphifyy[video]"` | +| `mcp` | שרת MCP stdio | `uv tool install "graphifyy[mcp]"` | +| `neo4j` | דחיפה ל-Neo4j | `uv tool install "graphifyy[neo4j]"` | +| `falkordb` | דחיפה ל-FalkorDB | `uv tool install "graphifyy[falkordb]"` | +| `svg` | ייצוא גרף ל-SVG | `uv tool install "graphifyy[svg]"` | +| `leiden` | זיהוי קהילות Leiden ‏(Python < 3.13 בלבד) | `uv tool install "graphifyy[leiden]"` | +| `ollama` | הרצה מקומית עם Ollama | `uv tool install "graphifyy[ollama]"` | +| `openai` | OpenAI / ממשקי API תואמי-OpenAI | `uv tool install "graphifyy[openai]"` | +| `gemini` | Google Gemini API | `uv tool install "graphifyy[gemini]"` | +| `anthropic` | Anthropic Claude API ‏(`--backend claude`, משתמש ב-`ANTHROPIC_API_KEY`) | `uv tool install "graphifyy[anthropic]"` | +| `bedrock` | AWS Bedrock (משתמש ב-IAM, ללא מפתח API) | `uv tool install "graphifyy[bedrock]"` | +| `azure` | Azure OpenAI Service ‏(`--backend azure`, משתמש ב-`AZURE_OPENAI_API_KEY` + ‏`AZURE_OPENAI_ENDPOINT`) | `uv tool install "graphifyy[openai]"` | +| `sql` | חילוץ סכמות SQL | `uv tool install "graphifyy[sql]"` | +| `postgres` | אינטרוספקציה של PostgreSQL חי (`--postgres DSN`) | `uv tool install "graphifyy[postgres]"` | +| `dm` | חילוץ AST של BYOND DreamMaker ‏`.dm`/`.dme` (עשוי לדרוש קומפיילר C + ‏`python3-dev` אם אין wheel מתאים לפלטפורמה) | `uv tool install "graphifyy[dm]"` | +| `terraform` | חילוץ AST של Terraform / HCL ‏`.tf`/`.tfvars`/`.hcl` | `uv tool install "graphifyy[terraform]"` | +| `chinese` | פילוח שאילתות בסינית (jieba) | `uv tool install "graphifyy[chinese]"` | +| `all` | כל מה שלמעלה | `uv tool install "graphifyy[all]"` | + +--- + +## גרמו לעוזר שלכם להשתמש בגרף תמיד + +הריצו פעם אחת בפרויקט אחרי בניית גרף: + +| פלטפורמה | פקודה | +|----------|---------| +| Claude Code | `graphify claude install` | +| CodeBuddy | `graphify codebuddy install` | +| Codex | `graphify codex install` | +| OpenCode | `graphify opencode install` | +| Kilo Code | `graphify kilo install` | +| GitHub Copilot CLI | `graphify copilot install` | +| VS Code Copilot Chat | `graphify vscode install` | +| Aider | `graphify aider install` | +| OpenClaw | `graphify claw install` | +| Factory Droid | `graphify droid install` | +| Trae | `graphify trae install` | +| Trae CN | `graphify trae-cn install` | +| Cursor | `graphify cursor install` | +| Gemini CLI | `graphify gemini install` | +| Hermes | `graphify hermes install` | +| Kimi Code | `graphify install --platform kimi` | +| Amp | `graphify amp install` | +| Agent Skills (חוצה-פלטפורמות) | `graphify agents install` (כינוי: `graphify skills install`) | +| Kiro IDE/CLI | `graphify kiro install` | +| Pi coding agent | `graphify pi install` | +| Devin CLI | `graphify devin install` | +| Google Antigravity | `graphify antigravity install` | + +הפקודה כותבת קובץ תצורה קטן שמנחה את העוזר שלכם להתייעץ עם גרף הידע בשאלות על בסיס הקוד — ולהעדיף שאילתות ממוקדות כמו `graphify query "<שאלה>"` על פני קריאת הדוח המלא או grep על קבצים גולמיים. בפלטפורמות שתומכות בהוקים נושאי-מטען (Claude Code, ‏Gemini CLI), הוק מופעל אוטומטית לפני קריאות כלי בסגנון חיפוש (וב-Claude Code גם לפני קריאת קובצי מקור אחד-אחד דרך הכלים Read/Glob) ומכוון את העוזר לנתיב הגרף. באחרות (Codex, ‏OpenCode, ‏Cursor וכו'), קובצי ההנחיות הקבועים (`AGENTS.md`, ‏`.cursor/rules/` וכו') מספקים את אותה הנחיית "קודם הגרף". ‏`GRAPH_REPORT.md` עדיין זמין לסקירת ארכיטקטורה רחבה. + +**CodeBuddy** עושה את אותם שני דברים כמו Claude Code: כותב קטע `CODEBUDDY.md` שמנחה את CodeBuddy לקרוא את `graphify-out/GRAPH_REPORT.md` לפני מענה על שאלות ארכיטקטורה, ומתקין **הוקים מסוג PreToolUse** ‏(`.codebuddy/settings.json`) שמופעלים לפני פקודות חיפוש ב-Bash וקריאת קבצים, ומכוונים ל-`graphify query` במקום. + +**Codex** כותב ל-`AGENTS.md` וגם מתקין **הוק PreToolUse** ב-`.codex/hooks.json` שמופעל לפני כל קריאת כלי Bash — אותו מנגנון קבוע כמו ב-Claude Code. + +להסרת graphify מכל הפלטפורמות בבת אחת: `graphify uninstall` (הוסיפו `--purge` כדי למחוק גם את `graphify-out/`). או השתמשו בפקודה הפר-פלטפורמית (למשל `graphify claude uninstall`). + +--- + +**Kilo Code** מתקין את מיומנות Graphify ל-`~/.config/kilo/skills/graphify/SKILL.md` ופקודת `‎/graphify` נטיבית ל-`~/.config/kilo/command/graphify.md`. ‏`graphify kilo install` כותב גם `AGENTS.md` וגם **תוסף `tool.execute.before` נטיבי** (`.kilo/plugins/graphify.js` + רישום ב-`.kilo/kilo.json` או `.kilo/kilo.jsonc`) כך ש-Kilo מקבל את אותה התנהגות תזכורת-גרף קבועה דרך תצורת `.kilo` נטיבית. + +**Cursor** כותב `.cursor/rules/graphify.mdc` עם `alwaysApply: true` — ‏Cursor מכליל אותו בכל שיחה אוטומטית, ללא צורך בהוק. + +## מה יש בדוח + +- **צומתי מפתח (God nodes)** — המושגים המקושרים ביותר בפרויקט. הכול עובר דרכם. +- **קשרים מפתיעים** — קישורים בין דברים שחיים בקבצים או מודולים שונים. מדורגים לפי מידת ההפתעה. +- **ה"למה"** — הערות בקוד (`# NOTE:`‏, `# WHY:`‏, `# HACK:`), ‏docstrings ורציונל עיצובי מהמסמכים מחולצים כצמתים נפרדים המקושרים לקוד שהם מסבירים. +- **שאלות מוצעות** — 4–5 שאלות שהגרף נמצא בעמדה ייחודית לענות עליהן. +- **תגי ביטחון** — כל קשר מוסק מסומן `EXTRACTED`‏, `INFERRED` או `AMBIGUOUS`. תמיד יודעים מה נמצא ומה נוחש. + +--- + +## אילו קבצים הוא מטפל + +| סוג | סיומות | +|------|-----------| +| קוד (36 דקדוקי tree-sitter) | `.py .ts .js .jsx .tsx .mjs .go .rs .java .c .cpp .h .hpp .cu .cuh .metal .rb .cs .kt .scala .php .swift .lua .luau .zig .ps1 .psm1 .ex .exs .m .mm .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .xaml .razor .cshtml` ‏(`.dm`/`.dme` דורש `uv tool install graphifyy[dm]`; ‏CUDA ‏`.cu`/`.cuh` ו-Metal ‏`.metal` משתמשים בדקדוק C++) | +| Salesforce Apex | `.cls .trigger` (מבוסס regex; מחלקות, ממשקים, enums, מתודות, טריגרים, קשתות SOQL/DML) | +| Terraform / HCL | `.tf .tfvars .hcl` (דורש `uv tool install graphifyy[terraform]`) | +| תצורות MCP | `.mcp.json` ‏`mcp.json` ‏`mcp_servers.json` ‏`claude_desktop_config.json` — מחלץ צומתי שרתים, הפניות לחבילות, דרישות משתני סביבה | +| מניפסטים של חבילות | `apm.yml` ‏`pyproject.toml` ‏`go.mod` ‏`pom.xml` — צומת חבילה קנוני אחד לכל חבילה (לפי שם) בתוספת קשתות `depends_on`, כך שחבילה שמופנית מכמה מניפסטים היא מוקד (hub) יחיד | +| מסמכים | `.md .mdx .qmd .html .txt .rst .yaml .yml` (קישורי markdown ‏`[text](./other.md)` ו-`[[wikilinks]]` הופכים לקשתות `references` בין מסמכים) | +| Office | `.docx .xlsx` (דורש `uv tool install graphifyy[office]`) | +| Google Workspace | `.gdoc .gsheet .gslides` ‏(opt-in; דורש אימות `gws` ו-`--google-workspace`; ‏Sheets דורש `uv tool install graphifyy[google]`) | +| PDF | `.pdf` | +| תמונות | `.png .jpg .webp .gif` | +| וידאו / אודיו | `.mp4 .mov .mp3 .wav` ועוד (דורש `uv tool install graphifyy[video]`) | +| YouTube / כתובות URL | כל כתובת וידאו (דורש `uv tool install graphifyy[video]`) | + +קוד מחולץ מקומית ללא קריאות API ‏(AST באמצעות tree-sitter). כל השאר עובר דרך ה-API של מודל עוזר ה-AI שלכם. + +קובצי `.gdoc`‏, `.gsheet` ו-`.gslides` של Google Drive לשולחן העבודה הם קיצורי דרך, לא תוכן מסמכים. כדי לכלול מסמכי Google Docs, ‏Sheets ו-Slides נטיביים בחילוץ headless, התקינו ואמתו את [`gws` CLI](https://github.com/googleworkspace/cli), ואז הריצו: + +
+ +```bash +uv tool install "graphifyy[google]" # נדרש לרינדור טבלאות Google Sheets +gws auth login -s drive +graphify extract ./docs --google-workspace +``` + +
+ +אפשר גם להגדיר `GRAPHIFY_GOOGLE_WORKSPACE=1`. ‏graphify מייצא קיצורי דרך אל `graphify-out/converted/` כקובצי Markdown נלווים, ואז מחלץ אותם. + +--- + +## פקודות נפוצות + +
+ +```bash +/graphify . # בניית גרף לתיקייה הנוכחית +/graphify ./docs --update # חילוץ מחדש של קבצים שהשתנו בלבד +/graphify . --cluster-only # הרצת אשכול מחדש בלי לחלץ מחדש +/graphify . --cluster-only --resolution 1.5 # קהילות מפורטות יותר +/graphify . --cluster-only --exclude-hubs 99 # הסתרת מוקדי-עזר מדירוג צומתי המפתח +/graphify . --no-viz # דילוג על ה-HTML, רק הדוח וה-JSON +/graphify . --wiki # בניית ויקי markdown מהגרף +graphify export callflow-html # ‏HTML ארכיטקטורה/זרימת-קריאות ב-Mermaid (מתחדש אוטומטית בכל commit אם ההוק מותקן) + +/graphify query "מה מחבר את האימות למסד הנתונים?" +/graphify path "UserService" "DatabasePool" +/graphify explain "RateLimiter" + +/graphify add https://arxiv.org/abs/1706.03762 # הבאת מאמר והוספתו +/graphify add # תמלול והוספת סרטון + +graphify hook install # בנייה מחדש אוטומטית בכל commit +graphify merge-graphs a.json b.json # מיזוג שני גרפים + +graphify prs # לוח PR: מצב CI, סטטוס ביקורת, מיפוי worktree +graphify prs 42 # צלילה עמוקה ל-PR ‏#42 עם השפעת גרף +graphify prs --triage # ‏AI מדרג את תור הביקורות שלכם (משתמש ב-backend המוגדר) +graphify prs --conflicts # ‏PRs שחולקים קהילות גרף — סיכון בסדר המיזוג +``` + +
+ +ראו את [רשימת הפקודות המלאה](#רשימת-הפקודות-המלאה) בהמשך. + +--- + +## התעלמות מקבצים + +צרו קובץ `.graphifyignore` בשורש הפרויקט — אותו תחביר כמו `.gitignore`, כולל שלילה עם `!`. + +**‏`.gitignore` נאכף אוטומטית.** ‏graphify קורא את ה-`.gitignore` בכל תיקייה. אם קיים גם `.graphifyignore`, השניים **ממוזגים** — תבניות `.graphifyignore` מוערכות אחרונות, כך שהן גוברות במקרה של התנגשות (כולל שלילות `!`). הוספת `.graphifyignore` רק מחריגה עוד; היא לעולם לא תחזיר קובץ שה-`.gitignore` כבר החריג. תחולת תת-תיקיות עובדת כמו ב-git — קובץ ignore משפיע רק על תת-העץ שלו. + +
+ +``` +# .graphifyignore +node_modules/ +dist/ +*.generated.py + +# לאנדקס רק את src/, להתעלם מכל השאר +* +!src/ +!src/** +``` + +
+ +--- + +## עבודת צוות + +‏`graphify-out/` מיועד להיכנס ל-git כדי שכל חברי הצוות יתחילו עם מפה מוכנה. + +**תוספות מומלצות ל-`.gitignore`:** + +
+ +``` +graphify-out/cost.json # מקומי בלבד +# graphify-out/cache/ # אופציונלי: commit למהירות, דילוג לשמירה על ריפו קטן +``` + +
+ +> ‏`manifest.json` הוא כעת נייד — המפתחות נשמרים כנתיבים יחסיים ומעוגנים מחדש בטעינה, כך שבטוח לבצע לו commit והדבר חוסך בנייה מלאה מחדש ב-checkout הראשון. + +**תהליך העבודה:** +1. אחד מחברי הצוות מריץ `‎/graphify .` ומבצע commit ל-`graphify-out/`. +2. כולם מושכים — העוזר שלהם קורא את הגרף מיד. +3. הריצו `graphify hook install` לבנייה אוטומטית מחדש אחרי כל commit ‏(AST בלבד, ללא עלות API). זה גם מגדיר merge driver של git כך ש-`graph.json` לעולם לא יישאר עם סימוני קונפליקט — שני מפתחים שמבצעים commit במקביל מקבלים מיזוג-איחוד אוטומטי של הגרפים. +4. כשמסמכים או מאמרים משתנים, הריצו `‎/graphify --update` לרענון הצמתים הללו. + +--- + +## שימוש ישיר בגרף + +
+ +```bash +# שאילתת גרף מהטרמינל +graphify query "הצג את זרימת האימות" +graphify query "מה מחבר בין DigestAuth ל-Response?" --graph graphify-out/graph.json + +# חשיפת הגרף כשרת MCP (לגישת כלים חוזרת) +python -m graphify.serve graphify-out/graph.json +python -m graphify.serve --graph graphify-out/graph.json # גם הדגל --graph מתקבל + +# רישום ב-Kimi Code: +kimi mcp add --transport stdio graphify -- python -m graphify.serve graphify-out/graph.json + +# או הגשה על HTTP כך שכל הצוות מצביע על URL אחד (בלי graphify מקומי): +python -m graphify.serve graphify-out/graph.json --transport http --port 8080 +python -m graphify.serve graphify-out/graph.json --transport http --host 0.0.0.0 --api-key "$SECRET" +``` + +
+ +שרת ה-MCP נותן לעוזר שלכם גישה מובנית: `query_graph`‏, `get_node`‏, `get_neighbors`‏, `shortest_path`‏, `list_prs`‏, `get_pr_impact`‏, `triage_prs`. + +### שרת HTTP משותף + +`--transport stdio` (ברירת המחדל) מריץ שרת מקומי אחד לכל מפתח. `--transport http` מגיש את אותם כלים על גבי MCP Streamable HTTP, כך שתהליך משותף אחד יכול לשרת את הגרף לכל הצוות — הלקוחות מכוונים את תצורת ה-MCP של ה-IDE אל `http://:8080/mcp` במקום להריץ graphify מקומית. + +| דגל | ברירת מחדל | תפקיד | +|---|---|---| +| `--transport {stdio,http}` | `stdio` | סוג התעבורה | +| `--host` | `127.0.0.1` | כתובת ההאזנה ל-HTTP (השתמשו ב-`0.0.0.0` לחשיפה מעבר ל-localhost) | +| `--port` | `8080` | פורט ההאזנה | +| `--api-key` | משתנה סביבה `GRAPHIFY_API_KEY` | דרישת `Authorization: Bearer ` (או `X-API-Key`) | +| `--path` | `/mcp` | נתיב העיגון ב-HTTP | +| `--json-response` | כבוי | החזרת JSON רגיל במקום זרמי SSE | +| `--stateless` | כבוי | ללא מצב פר-סשן (לפריסות מאוזנות-עומס / CI) | +| `--session-timeout` | `3600` | ניקוי סשנים לא פעילים אחרי N שניות (`0` מבטל) | + +ברירת המחדל `127.0.0.1` היא loopback בלבד. הגדירו `--host 0.0.0.0` **וגם** `--api-key` יחד כשחושפים על מארח משותף. אפשר להריץ בקונטיינר: + +
+ +```bash +docker build -t graphify . +docker run -p 8080:8080 -v "$(pwd)/graphify-out:/data" graphify \ + /data/graph.json --transport http --host 0.0.0.0 --api-key "$SECRET" +``` + +
+ +> **הערת WSL / לינוקס:** אובונטו מגיעה עם `python3`, לא `python`. השתמשו ב-venv כדי להימנע מהתנגשויות: + +
+ +```bash +python3 -m venv .venv && .venv/bin/pip install "graphifyy[mcp]" +``` + +
+ +--- + +## משתני סביבה + +אלו נדרשים רק ל**חילוץ headless / CI** ‏(`graphify extract`). בהרצה דרך מיומנות `‎/graphify` בתוך ה-IDE, ה-API של המודל מסופק על ידי סשן ה-IDE — לא נדרשים מפתחות נוספים. + +| משתנה | משמש עבור | מתי נדרש | +|---|---|---| +| `ANTHROPIC_API_KEY` | ‏backend של Claude ‏(Anthropic) | `--backend claude` | +| `ANTHROPIC_BASE_URL` | כתובת endpoint תואם-Anthropic ‏(פרוקסי LiteLLM, שערים, ...) | `--backend claude` (ברירת מחדל: `https://api.anthropic.com`) | +| `ANTHROPIC_MODEL` | שם המודל ל-backend של Claude — ל-endpoints מותאמים, השתמשו בשם/כינוי שהשרת שלכם חושף | `--backend claude` (ברירת מחדל: `claude-sonnet-4-6`) | +| `GEMINI_API_KEY` או `GOOGLE_API_KEY` | ‏backend של Google Gemini | `--backend gemini` | +| `OPENAI_API_KEY` | ‏OpenAI או ממשקים תואמי-OpenAI | `--backend openai` (שרתים מקומיים מקבלים כל ערך לא ריק) | +| `OPENAI_BASE_URL` | כתובת שרת תואם-OpenAI ‏(llama.cpp, ‏vLLM, ‏LM Studio, ...) | `--backend openai` (ברירת מחדל: `https://api.openai.com/v1`) | +| `OPENAI_MODEL` | שם המודל ל-backend של OpenAI — לשרתים בהרצה עצמית, השתמשו בשם/כינוי שהשרת חושף (בדקו את endpoint ה-`/v1/models` שלו) | `--backend openai` (ברירת מחדל: `gpt-4.1-mini`) | +| `DEEPSEEK_API_KEY` | ‏backend של DeepSeek | `--backend deepseek` | +| `MOONSHOT_API_KEY` | ‏backend של Kimi Code | `--backend kimi` | +| `OLLAMA_BASE_URL` | כתובת הרצה מקומית של Ollama | `--backend ollama` (ברירת מחדל: `http://localhost:11434`) | +| `OLLAMA_MODEL` | שם מודל Ollama | `--backend ollama` (ברירת מחדל: זיהוי אוטומטי) | +| `GRAPHIFY_OLLAMA_NUM_CTX` | דריסת גודל חלון ה-KV-cache של Ollama | אופציונלי — מותאם אוטומטית כברירת מחדל | +| `GRAPHIFY_OLLAMA_KEEP_ALIVE` | דקות להשארת מודל Ollama טעון | אופציונלי — ‏`0` לפריקה אחרי כל chunk | +| `AZURE_OPENAI_API_KEY` | ‏backend של Azure OpenAI Service | `--backend azure` | +| `AZURE_OPENAI_ENDPOINT` | כתובת ה-endpoint של משאב Azure | `--backend azure` (נדרש יחד עם מפתח ה-API) | +| `AZURE_OPENAI_API_VERSION` | דריסת גרסת ה-API של Azure | אופציונלי — ברירת מחדל `2024-12-01-preview` | +| `AZURE_OPENAI_DEPLOYMENT` או `GRAPHIFY_AZURE_MODEL` | שם הפריסה ב-Azure | אופציונלי — ברירת מחדל `gpt-4o` | +| `AWS_*` / `~/.aws/credentials` | ‏AWS Bedrock — שרשרת אישורים סטנדרטית | `--backend bedrock` (ללא מפתח API, משתמש ב-IAM) | +| `GRAPHIFY_MAX_WORKERS` | מספר threads למקביליות AST | אופציונלי — גם דגל `--max-workers` | +| `GRAPHIFY_MAX_OUTPUT_TOKENS` | העלאת תקרת הפלט לקורפוסים צפופים | אופציונלי — למשל `32768` לקבצים גדולים | +| `GRAPHIFY_API_TIMEOUT` | ‏timeout לקריאה בשניות עבור HTTP, ‏claude-cli ו-Anthropic SDK ‏(ברירת מחדל: 600) | אופציונלי — גם דגל `--api-timeout` | +| `GRAPHIFY_MAX_RETRIES` | מספר ניסיונות חוזרים לבקשה שנחסמה בקצב (429) לפני ויתור (ברירת מחדל: 6; מכבד `Retry-After`) | אופציונלי — העלו למגבלות ארגוניות נוקשות; ‏`0` מבטל | +| `GRAPHIFY_FORCE` | כפיית בנייה מחדש של הגרף גם עם פחות צמתים | אופציונלי — גם דגל `--force` | +| `GRAPHIFY_GOOGLE_WORKSPACE` | הפעלה אוטומטית של ייצוא Google Workspace | אופציונלי — הגדירו `1` | +| `GRAPHIFY_TRIAGE_BACKEND` | ‏backend עבור `graphify prs --triage` | אופציונלי — מזוהה אוטומטית מהמפתחות הזמינים | +| `GRAPHIFY_TRIAGE_MODEL` | דריסת מודל לטריאז' | אופציונלי — למשל `claude-opus-4-7` | +| `GRAPHIFY_QUERY_LOG` | דריסת נתיב יומן השאילתות (ברירת מחדל: `~/.cache/graphify-queries.log`) | אופציונלי — ערך ריק או `/dev/null` להשתקה | +| `GRAPHIFY_QUERY_LOG_DISABLE` | הגדירו `1` לביטול מוחלט של יומן השאילתות | אופציונלי | +| `GRAPHIFY_QUERY_LOG_RESPONSES` | הגדירו `1` לרישום גם של תשובות תת-גרף מלאות (כבוי כברירת מחדל) | אופציונלי | +| `GRAPHIFY_MAX_GRAPH_BYTES` | דריסת תקרת הגודל של graph.json ‏(512 MiB) — למשל `700MB`, ‏`2GB` או בייטים | אופציונלי — שימושי לקורפוסים גדולים מאוד | +| `GRAPHIFY_LLM_TEMPERATURE` | דריסת טמפרטורת ה-LLM לחילוץ סמנטי — למשל `0.7`, או `none` להשמטה | אופציונלי — מושמט אוטומטית למודלי היסק o1/o3/o4/gpt-5 | + +--- + +## פרטיות + +- **קובצי קוד** — מעובדים מקומית עם tree-sitter. שום דבר לא עוזב את המחשב. קורפוס של קוד בלבד אינו דורש מפתח API — ‏`graphify extract` רץ לגמרי offline. +- **וידאו / אודיו** — מתומלל מקומית עם faster-whisper. שום דבר לא עוזב את המחשב. +- **מסמכים, PDF, תמונות** — נשלחים לעוזר ה-AI שלכם לחילוץ סמנטי (דרך מיומנות `‎/graphify`, עם המודל שסשן ה-IDE שלכם מריץ). ‏`graphify extract` בחילוץ headless דורש `GEMINI_API_KEY` / ‏`GOOGLE_API_KEY` ‏(Gemini), ‏`MOONSHOT_API_KEY` ‏(Kimi), ‏`ANTHROPIC_API_KEY` ‏(Claude), ‏`OPENAI_API_KEY` ‏(OpenAI), ‏`DEEPSEEK_API_KEY` ‏(DeepSeek), מופע Ollama רץ (`OLLAMA_BASE_URL`), אישורי AWS דרך שרשרת הספקים הסטנדרטית (Bedrock — ללא מפתח API, משתמש ב-IAM), או קובץ ההרצה `claude` ‏(Claude Code — ללא מפתח API, משתמש במנוי Claude שלכם). הדגל `--dedup-llm` משתמש באותו מפתח. +- **מיקום נתונים (Data residency)** — ‏`graphify extract` מזהה אוטומטית באיזה ספק להשתמש לפי המפתח שמוגדר (עדיפות: Gemini ‏→ Kimi ‏→ Claude ‏→ OpenAI ‏→ DeepSeek ‏→ Azure ‏→ Bedrock ‏→ Ollama). לקוד עם דרישות מיקום נתונים, השתמשו ב-`--backend ollama` (מקומי לחלוטין) או העבירו דגל `--backend` מפורש. ‏Kimi ‏(`MOONSHOT_API_KEY`) מנתב לשרתי Moonshot AI בסין. +- ללא טלמטריה, ללא מעקב שימוש, ללא אנליטיקה. +- **יומן שאילתות** — כל קריאת `graphify query`‏, `graphify path`‏, `graphify explain` ו-`query_graph` של MCP נרשמת ל-`~/.cache/graphify-queries.log` בפורמט JSON Lines ‏(חותמת זמן, שאלה, קורפוס, צמתים שהוחזרו, משך). תשובות תת-גרף מלאות **אינן** נשמרות כברירת מחדל. הגדירו `GRAPHIFY_QUERY_LOG_DISABLE=1` לביטול, או `GRAPHIFY_QUERY_LOG=/dev/null` להשתקה בלי לבטל את המנגנון. + +--- + +## פתרון בעיות + +**‏`graphify: command not found` אחרי ההתקנה** +ה-CLI מותקן אבל תיקיית ה-bin שלו אינה ב-`PATH` של המעטפת. בחרו את התיקון לפי אופן ההתקנה: +- **uv** ‏(`uv tool install graphifyy`): הפקודה מגיעה לתיקיית הכלים של uv ‏(`~/.local/bin`), שהתקנת macOS/zsh טרייה לרוב לא כוללת ב-`PATH`. הריצו `uv tool update-shell` ופתחו טרמינל חדש. (מצאו את התיקייה עם `uv tool dir --bin`.) +- **pipx** ‏(`pipx install graphifyy`): הריצו `pipx ensurepath` ופתחו טרמינל חדש. +- **pip** ‏(`pip install graphifyy`): ‏pip מתקין סקריפטים לתיקיית bin של המשתמש שאולי אינה ב-`PATH` — הוסיפו את `~/Library/Python/3.x/bin` ‏(macOS) או `~/.local/bin` ‏(לינוקס) ל-`PATH` ב-`~/.zshrc`/`~/.bashrc`, או פשוט הריצו `python -m graphify`. + +**‏`uvx graphify …` או `uv tool run graphify …` לא מצליחים לפתור את `graphify`** +חבילת ה-PyPI היא `graphifyy`; ‏`graphify` הוא רק הפקודה שהיא מספקת. ‏`uv tool run` מתייחס למילה הראשונה כשם *חבילה*, מחפש חבילה בשם `graphify` ומדווח `No solution found … no versions of graphify`. ציינו את החבילה במפורש: `uvx --from graphifyy graphify install` (זהה ל-`uv tool run --from graphifyy graphify install`). או התקינו פעם אחת עם `uv tool install graphifyy` וקראו ל-`graphify` ישירות. + +**‏`python -m graphify` עובד אבל פקודת `graphify` לא** +ה-`PATH` של המעטפת לא כולל את תיקיית ה-bin שאליה הותקנה הפקודה. העדיפו `uv tool install` / ‏`pipx install` על פני `pip` רגיל, ואז הריצו `uv tool update-shell` / ‏`pipx ensurepath` ופתחו טרמינל חדש (ראו הערות ההתקנה לעיל). + +**‏`‎/graphify .` גורם ל-"path not recognized" ב-PowerShell** +‏PowerShell מתייחס ל-`/` מוביל כמפריד נתיבים. השתמשו ב-`graphify .` (בלי לוכסן) ב-Windows. + +**לגרף יש פחות צמתים אחרי `--update` או בנייה מחדש** +אם refactor מחק קבצים, הצמתים הישנים נשארים. העבירו `--force` (או הגדירו `GRAPHIFY_FORCE=1`) לדריסה גם כשהבנייה החדשה קטנה יותר. + +**לגרף יש צמתים כפולים לאותה ישות (ghost duplicates)** +כפילויות רפאים (אותו סימבול מופיע פעמיים — פעם מחילוץ AST עם מיקום מקור, ופעם מחילוץ סמנטי בלעדיו) ממוזגות כעת אוטומטית בזמן הבנייה. אם אתם רואים זאת בגרף שנבנה לפני v0.8.33, הריצו חילוץ מלא מחדש לניקוי: + +
+ +```bash +graphify extract . --force +``` + +
+ +**‏Ollama נגמר לו ה-VRAM / חריגה מחלון ההקשר** +חלון ה-KV-cache מותאם אוטומטית אך עשוי להיות גדול מדי ל-GPU שלכם. הקטינו אותו: + +
+ +```bash +GRAPHIFY_OLLAMA_NUM_CTX=8192 graphify extract ./docs --backend ollama --token-budget 4000 +``` + +
+ +**אזהרות `LLM returned invalid JSON` / ‏`Unterminated string`** +תשובת ה-JSON של המודל פגעה במגבלת אסימוני הפלט ונחתכה באמצע מחרוזת. ‏graphify מתאושש אוטומטית (הוא מפצל את ה-chunk ומחלץ מחדש את החצאים, ומסמך יחיד גדול מדי נחתך תחילה בגבולות כותרות/פסקאות כך שהקובץ כולו עדיין מכוסה), כך שהאזהרות רועשות אך אין אובדן נתונים. להפחתת התופעה, העלו את תקרת הפלט או הקטינו את פלט ה-chunk: + +
+ +```bash +GRAPHIFY_MAX_OUTPUT_TOKENS=16384 graphify extract . --mode deep # הרמת התקרה +graphify extract . --mode deep --token-budget 4000 # קלטים קטנים יותר -> פלט קטן יותר +``` + +
+ +עם שער ענן כמו OpenRouter, העדיפו `--backend openai` (הגדירו `OPENAI_BASE_URL`) על פני שכבת ה-Ollama — זה נתיב תואם-OpenAI נקי יותר. אם למודל יש תקרת פלט משלו, הורדת `--token-budget` היא המנוף האמין. + +**‏HTML של הגרף גדול מדי לפתיחה בדפדפן (מעל 5000 צמתים)** +דלגו על יצירת ה-HTML והשתמשו ב-JSON ישירות: + +
+ +```bash +graphify cluster-only ./my-project --no-viz +graphify query "..." +``` + +
+ +**ל-`graph.json` יש סימוני קונפליקט אחרי ששני מפתחים ביצעו commit במקביל** +הריצו `graphify hook install` — הוא מגדיר merge driver של git שממזג-מאחד את `graph.json` אוטומטית כך שקונפליקטים לא קורים בכלל. + +**החילוץ מחזיר צמתים/קשתות ריקים למסמכים או PDF** +מסמכים, PDF ותמונות דורשים קריאת LLM — קורפוסים של קוד בלבד אינם דורשים מפתח. ודאו שמפתח ה-API מוגדר וה-backend נכון: + +
+ +```bash +ANTHROPIC_API_KEY=sk-... graphify extract ./docs --backend claude +``` + +
+ +**אזהרת חוסר התאמה בגרסת המיומנות ב-IDE** +גרסת graphify המותקנת שונה מקובץ המיומנות. עדכנו: + +
+ +```bash +uv tool upgrade graphifyy +graphify install # דורס את קובץ המיומנות +``` + +
+ +--- + +## רשימת הפקודות המלאה + +
+ +``` +/graphify # הרצה על התיקייה הנוכחית +/graphify ./raw # הרצה על תיקייה מסוימת +/graphify ./raw --mode deep # חילוץ קשרים אגרסיבי יותר +/graphify ./raw --update # חילוץ מחדש של קבצים שהשתנו בלבד +/graphify ./raw --directed # שמירת כיוון הקשתות +/graphify ./raw --cluster-only # הרצת אשכול מחדש על גרף קיים +/graphify ./raw --no-viz # דילוג על ויזואליזציית HTML +/graphify ./raw --obsidian # יצירת כספת Obsidian +/graphify ./raw --obsidian --obsidian-dir ~/vault # כתיבה לכספת קיימת (לעולם לא דורס פתקים או תצורת .obsidian שלכם) +/graphify ./raw --wiki # בניית ויקי markdown שסוכנים יכולים לסרוק +/graphify ./raw --svg # ייצוא graph.svg +/graphify ./raw --graphml # ייצוא ל-Gephi / yEd +/graphify ./raw --neo4j # יצירת cypher.txt ל-Neo4j +/graphify ./raw --neo4j-push bolt://localhost:7687 +/graphify ./raw --falkordb # יצירת cypher.txt ל-FalkorDB +/graphify ./raw --falkordb-push falkordb://localhost:6379 +/graphify ./raw --watch # סנכרון אוטומטי כשקבצים משתנים +/graphify ./raw --mcp # הפעלת שרת MCP stdio + +/graphify add https://arxiv.org/abs/1706.03762 +/graphify add +/graphify add https://... --author "Name" --contributor "Name" + +/graphify query "מה מחבר את ה-attention לאופטימייזר?" +/graphify query "..." --dfs --budget 1500 +/graphify path "DigestAuth" "Response" +/graphify explain "SwinTransformer" + +graphify save-result --question "Q" --answer "A" --nodes Foo Bar --outcome useful # תיעוד תוצאת שאלה-תשובה (זיכרון עבודה; outcome ∈ useful|dead_end|corrected) +graphify reflect # איחוד תוצאות graphify-out/memory/ אל reflections/LESSONS.md +graphify reflect --if-stale # לא עושה דבר אם LESSONS.md כבר חדש מכל הקלטים (זול להרצה בכל סשן) +graphify reflect --out docs/LESSONS.md # כתיבת מסמך הלקחים למקום אחר +graphify reflect --graph graphify-out/graph.json # קיבוץ לקחים לפי קהילה + כתיבת שכבת זיכרון העבודה (.graphify_learning.json) + # השכבה מתייגת צמתים preferred/tentative/contested (משוקלל-עדכניות, עם מקור); + # graphify explain / query מציגים אז רמז "Lesson:", מסומן "code changed — re-verify" כשהמקור התקדם + +graphify uninstall # הסרה מכל הפלטפורמות במכה אחת +graphify uninstall --purge # מחיקה גם של graphify-out/ +graphify uninstall --project --platform codex # הסרת קובצי התקנה ברמת פרויקט בלבד + +graphify hook install # הוקים של post-commit + post-checkout +graphify hook uninstall +graphify hook status + +# הנחיות עוזר קבועות - פר פלטפורמה +graphify claude install # CLAUDE.md + הוק PreToolUse ‏(Claude Code) +graphify claude uninstall +graphify codebuddy install # CODEBUDDY.md + הוק PreToolUse ‏(CodeBuddy) +graphify codebuddy uninstall +graphify codex install # AGENTS.md + הוק PreToolUse ב-.codex/hooks.json ‏(Codex) +graphify opencode install # AGENTS.md + תוסף tool.execute.before ‏(OpenCode) +graphify kilo install # מיומנות Kilo נטיבית + פקודת /graphify + ‏AGENTS.md + תוסף .kilo +graphify kilo uninstall +graphify cursor install # .cursor/rules/graphify.mdc ‏(Cursor) +graphify cursor uninstall +graphify gemini install # GEMINI.md + הוק BeforeTool ‏(Gemini CLI) +graphify gemini uninstall +graphify copilot install # קובץ מיומנות (GitHub Copilot CLI) +graphify copilot uninstall +graphify aider install # AGENTS.md ‏(Aider) +graphify aider uninstall +graphify claw install # AGENTS.md ‏(OpenClaw) +graphify claw uninstall +graphify droid install # AGENTS.md ‏(Factory Droid) +graphify droid uninstall +graphify trae install # AGENTS.md ‏(Trae) +graphify trae uninstall +graphify trae-cn install # AGENTS.md ‏(Trae CN) +graphify trae-cn uninstall +graphify hermes install # AGENTS.md + ~/.hermes/skills/ ‏(Hermes) +graphify hermes uninstall +graphify amp install # קובץ מיומנות (Amp) +graphify amp uninstall +graphify agents install # ~/.agents/skills/ + AGENTS.md ‏(חוצה-פלטפורמות; כינוי: graphify skills) +graphify agents uninstall +graphify kiro install # .kiro/skills/ + .kiro/steering/graphify.md ‏(Kiro IDE/CLI) +graphify kiro uninstall +graphify pi install # קובץ מיומנות (Pi coding agent) +graphify pi uninstall +graphify devin install # קובץ מיומנות + .windsurf/rules/graphify.md ‏(Devin CLI) +graphify devin uninstall +graphify antigravity install # .agents/rules + .agents/workflows ‏(Google Antigravity) +graphify antigravity uninstall + +graphify extract ./docs # חילוץ LLM ‏headless ל-CI ‏(ללא IDE) +graphify extract ./docs --backend gemini # ‏backend מפורש: gemini, kimi, claude, openai, deepseek, ollama, bedrock או claude-cli +graphify extract ./docs --backend gemini --model gemini-3.1-pro-preview +graphify extract ./docs --backend ollama # ‏Ollama מקומי (הגדירו OLLAMA_BASE_URL / OLLAMA_MODEL) - ללא מפתח API ל-loopback +OPENAI_BASE_URL=http://localhost:8080/v1 OPENAI_MODEL=my-model graphify extract ./docs --backend openai # כל שרת תואם-OpenAI ‏(llama.cpp, vLLM, LM Studio) +ANTHROPIC_BASE_URL=http://localhost:4000 ANTHROPIC_MODEL=my-model graphify extract ./docs --backend claude # כל endpoint תואם-Anthropic ‏(פרוקסי LiteLLM, שערים) +GRAPHIFY_OLLAMA_NUM_CTX=32768 graphify extract ./docs --backend ollama # דריסת חלון ה-KV-cache (מותאם אוטומטית כברירת מחדל) +GRAPHIFY_OLLAMA_KEEP_ALIVE=0 graphify extract ./docs --backend ollama # פריקת המודל אחרי כל chunk (חוסך VRAM ב-GPU קטן) +graphify extract ./docs --backend bedrock # AWS Bedrock דרך IAM - ללא מפתח API, שרשרת אישורי AWS +graphify extract ./docs --backend claude-cli # ניתוב דרך Claude Code CLI - ללא מפתח API, דרך מנוי Claude שלכם +graphify extract ./docs --backend azure # Azure OpenAI (הגדירו AZURE_OPENAI_API_KEY + AZURE_OPENAI_ENDPOINT) +graphify extract ./docs --max-workers 16 # מקביליות AST ‏(גם GRAPHIFY_MAX_WORKERS) +graphify extract --postgres "postgresql://user:pass@host/db" # אינטרוספקציה ישירה של סכמת PostgreSQL חיה +graphify extract ./my-workspace --cargo # אינטרוספקציה ישירה של תלויות Cargo workspace ברוסט +graphify extract ./docs --token-budget 30000 # ‏chunks סמנטיים קטנים יותר למודלים מקומיים/קטנים +graphify extract ./docs --max-concurrency 2 # פחות קריאות LLM מקביליות (שימושי להרצה מקומית) +graphify extract ./docs --api-timeout 900 # ‏timeout ארוך יותר למודלים מקומיים איטיים (ברירת מחדל 600 שניות) +graphify extract ./docs --google-workspace # ייצוא .gdoc/.gsheet/.gslides דרך gws לפני החילוץ +graphify extract ./docs --mode deep # חילוץ סמנטי עשיר יותר עם system prompt מורחב +graphify extract ./docs --no-cluster # חילוץ גולמי בלבד, דילוג על אשכול +graphify extract ./docs --timing # הדפסת זמני ריצה פר-שלב ל-stderr ‏(עובד גם ב-cluster-only) +graphify extract ./docs --force # דריסת graph.json גם אם לגרף החדש פחות צמתים (אחרי refactors או לניקוי כפילויות רפאים) +graphify extract ./docs --dedup-llm # ‏LLM כמכריע לזוגות ישויות עמומים (משתמש באותו מפתח API) +graphify extract ./docs --global --as myrepo # חילוץ ורישום בגרף הגלובלי חוצה-הפרויקטים +GRAPHIFY_MAX_OUTPUT_TOKENS=32768 graphify extract ./docs --backend claude # הרמת תקרת הפלט לקורפוסים צפופים + +graphify export callflow-html # graphify-out/-callflow.html +graphify export callflow-html --max-sections 8 # הגבלת מספר קטעי הארכיטקטורה שנוצרים +graphify export callflow-html --output docs/arch.html +graphify export callflow-html ./some-repo/graphify-out + +graphify global add graphify-out/graph.json --as myrepo # רישום גרף פרויקט אל ~/.graphify/global-graph.json +graphify global remove myrepo # הסרת פרויקט מהגרף הגלובלי +graphify global list # הצגת כל המאגרים הרשומים + ספירות צמתים/קשתות +graphify global path # הדפסת הנתיב לקובץ הגרף הגלובלי + +graphify prs # לוח PR: ‏CI, ביקורת, worktree, השפעת גרף +graphify prs 42 # צלילה עמוקה ל-PR ‏#42 +graphify prs --triage # דירוג טריאז' AI ‏(מזהה backend אוטומטית מהסביבה) +graphify prs --worktrees # מיפוי worktree ← branch ← PR +graphify prs --conflicts # ‏PRs שחולקים קהילות גרף (סיכון בסדר המיזוג) +graphify prs --base main # סינון ל-PRs שמכוונים ל-branch בסיס מסוים +graphify prs --repo owner/repo # הרצה מול מאגר GitHub אחר +GRAPHIFY_TRIAGE_BACKEND=kimi graphify prs --triage # ‏backend מסוים לטריאז' + +graphify clone https://github.com/karpathy/nanoGPT +graphify merge-graphs a.json b.json --out merged.json +graphify --version # הדפסת הגרסה המותקנת +graphify watch ./src +graphify check-update ./src +graphify update ./src +graphify update ./src --no-cluster # דילוג על אשכול מחדש, כתיבת גרף AST גולמי בלבד +graphify update ./src --force # דריסה גם אם לגרף החדש פחות צמתים +graphify cluster-only ./my-project +graphify cluster-only ./my-project --graph path/to/graph.json # מיקום גרף מותאם +graphify cluster-only ./my-project --max-concurrency 16 --batch-size 200 # תיוג קהילות מקבילי (גרפים גדולים) +graphify cluster-only ./my-project --resolution 1.5 # יותר קהילות, קטנות יותר +graphify cluster-only ./my-project --exclude-hubs 99 # החרגת צומתי p99 מהחלוקה +graphify cluster-only ./my-project --no-label # השארת מצייני "Community N" +graphify cluster-only ./my-project --backend=gemini # ‏backend לשמות הקהילות +graphify cluster-only ./my-project --backend=gemini --model gemini-2.5-pro # מודל מסוים +graphify label ./my-project # מתן שמות (מחדש) לקהילות עם ה-backend המוגדר +graphify label ./my-project --backend=openai --model gpt-4o # כפיית backend ומודל מסוימים +``` + +
+ +> **שמות קהילות:** בתוך סוכן (Claude Code, ‏Gemini CLI) הסוכן נותן שמות לקהילות בעצמו. בהרצת ה-CLI החשוף, `cluster-only` נותן שמות אוטומטית עם ה-backend המוגדר (מובנה או ספק תואם-OpenAI מותאם) — העבירו `--no-label` להשארת `Community N`, או הריצו `graphify label` ליצירת שמות מחדש לפי דרישה. + +--- + +## ללמוד עוד + +- [איך זה עובד](../how-it-works.md) — צינור החילוץ, זיהוי קהילות, ניקוד ביטחון, מדדים +- [ARCHITECTURE.md](../../ARCHITECTURE.md) — פירוק מודולים, איך מוסיפים שפה +- [אינטגרציות אופציונליות](../docker-mcp-sqlite.md) — ‏Docker MCP Toolkit + ‏SQLite + +--- + +## נבנה על graphify — ‏Penpax + +[**Penpax**](https://graphify.com) היא השכבה התמידית שנבנתה מעל graphify — היא מיישמת את אותה גישת גרף על כל חיי העבודה שלכם: פגישות, היסטוריית דפדפן, מיילים, קבצים וקוד, ומתעדכנת ברציפות ברקע. + +נבנתה לאנשים שהעבודה שלהם מפוזרת על פני מאות שיחות ומסמכים שהם לעולם לא יוכלו לשחזר במלואם. ללא ענן, לגמרי על המכשיר. + +**גרסת ניסיון חינמית תושק בקרוב.** [הצטרפו לרשימת ההמתנה ←](https://graphify.com) + +--- + +
+תרומה לפרויקט + +### הקמת סביבת פיתוח + +הפרויקט משתמש ב-[uv](https://docs.astral.sh/uv/) לתהליך הפיתוח. התקינו אותו פעם אחת, ואז: + +
+ +```bash +git clone https://github.com/safishamsi/graphify.git +cd graphify +git checkout v8 # ‏branch הפיתוח הפעיל + +# יצירת venv לפרויקט והתקנת graphify + כל התוספים + קבוצת dev +# ‏(pytest). ‏uv מתקין את קבוצת התלויות dev כברירת מחדל; העבירו --no-dev לדילוג. +uv sync --all-extras +``` + +
+ +אימות ההתקנה במצב עריכה: + +
+ +```bash +uv run graphify --version +uv run python -c "import graphify; print(graphify.__file__)" +``` + +
+ +### הרצת בדיקות + +
+ +```bash +uv run pytest tests/ -q # הרצת החבילה המלאה +uv run pytest tests/test_extract.py -q # מודול אחד +uv run pytest tests/ -q -k "python" # סינון לפי שם +``` + +
+ +> הערת macOS: חבילת הבדיקות כוללת גם `sample.f90` וגם `sample.F90`. אלו מתנגשים במערכות קבצים HFS+ / APFS שאינן רגישות לרישיות. הריצו בלינוקס או בקונטיינר Docker אם צריך לבדוק את שתי גרסאות ה-Fortran יחד. + +### תהליך עבודה ב-Git + +- הפיתוח הפעיל מתרחש ב-branch ‏`v8`. +- סגנון commit: ‏`fix: ` / ‏`feat: ` / ‏`docs: ` +- לפני פתיחת PR, הריצו `uv run pytest tests/ -q` וודאו שהוא עובר. +- הוסיפו קובץ fixture ל-`tests/fixtures/` ובדיקות ל-`tests/test_languages.py` לכל מחלץ שפה חדש. + +### מה כדאי לתרום + +**דוגמאות עבודה (worked examples)** הן התרומה השימושית ביותר. הריצו `‎/graphify` על קורפוס אמיתי, שמרו את הפלט ב-`worked/{slug}/`, כתבו `review.md` כן שמכסה מה הגרף קלע ומה פספס, ופתחו PR. + +**באגים בחילוץ** — פתחו issue עם קובץ הקלט, רשומת ה-cache ‏(`graphify-out/cache/`) ומה חסר או שגוי. + +ראו [ARCHITECTURE.md](../../ARCHITECTURE.md) לאחריות המודולים ואיך מוסיפים שפה. + + diff --git a/docs/translations/README.hi-IN.md b/docs/translations/README.hi-IN.md index 4dfa01b64..181963c29 100644 --- a/docs/translations/README.hi-IN.md +++ b/docs/translations/README.hi-IN.md @@ -11,7 +11,7 @@ PyPI Downloads Sponsor - LinkedIn + LinkedIn

**एक AI कोडिंग असिस्टेंट स्किल।** Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro या Google Antigravity में `/graphify` टाइप करें — यह आपकी फ़ाइलें पढ़ता है, एक नॉलेज ग्राफ बनाता है, और आपको वह संरचना वापस देता है जो आप नहीं जानते थे कि मौजूद है। कोडबेस को तेज़ी से समझें। आर्किटेक्चरल निर्णयों के पीछे का "क्यों" खोजें। diff --git a/docs/translations/README.pt-BR.md b/docs/translations/README.pt-BR.md index f145ec036..37e7d3f6e 100644 --- a/docs/translations/README.pt-BR.md +++ b/docs/translations/README.pt-BR.md @@ -11,7 +11,7 @@ PyPI Downloads Sponsor - LinkedIn + LinkedIn

**Uma habilidade para assistentes de código IA.** Digite `/graphify` no Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro ou Google Antigravity — ele lê seus arquivos, constrói um grafo de conhecimento e devolve a você estrutura que você não sabia que existia. Entenda uma base de código mais rapidamente. Encontre o "porquê" por trás das decisões arquiteturais. diff --git a/docs/translations/README.ru-RU.md b/docs/translations/README.ru-RU.md index c06b68251..7518aea41 100644 --- a/docs/translations/README.ru-RU.md +++ b/docs/translations/README.ru-RU.md @@ -11,7 +11,7 @@ PyPI Downloads Sponsor - LinkedIn + LinkedIn

**Навык для AI-ассистента по написанию кода.** Введите `/graphify` в Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro или Google Antigravity — он прочитает ваши файлы, построит граф знаний и вернёт вам структуру, о существовании которой вы не подозревали. Понимайте кодовую базу быстрее. Находите «почему» за архитектурными решениями. diff --git a/docs/translations/README.uk-UA.md b/docs/translations/README.uk-UA.md index 41828d266..2fa632544 100644 --- a/docs/translations/README.uk-UA.md +++ b/docs/translations/README.uk-UA.md @@ -1,5 +1,5 @@

- Graphify + Graphify

@@ -13,7 +13,7 @@ PyPI Downloads Sponsor - LinkedIn + LinkedIn X

@@ -529,11 +529,11 @@ graphify cluster-only ./my-project --exclude-hubs 99 # виключи ## Побудовано на graphify — Penpax -[**Penpax**](https://graphifylabs.ai) — це завжди активний шар поверх graphify, він застосовує той самий графовий підхід до всього робочого життя: зустрічей, історії браузера, email-ів, файлів і коду, постійно оновлюючись у фоновому режимі. +[**Penpax**](https://graphify.com) — це завжди активний шар поверх graphify, він застосовує той самий графовий підхід до всього робочого життя: зустрічей, історії браузера, email-ів, файлів і коду, постійно оновлюючись у фоновому режимі. Створений для людей, чия робота розкидана по сотнях розмов і документів, які неможливо повністю відтворити. Без хмари, повністю на пристрої. -**Безкоштовна пробна версія незабаром.** [Приєднайтесь до списку очікування →](https://graphifylabs.ai) +**Безкоштовна пробна версія незабаром.** [Приєднайтесь до списку очікування →](https://graphify.com) --- diff --git a/docs/translations/README.uz-UZ.md b/docs/translations/README.uz-UZ.md index d04b70b65..0e5f6e745 100644 --- a/docs/translations/README.uz-UZ.md +++ b/docs/translations/README.uz-UZ.md @@ -11,7 +11,7 @@ PyPI Downloads Sponsor - LinkedIn + LinkedIn

**Sun'iy intellektga asoslangan kod yordamchilari uchun ko'nikma.** Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro yoki Google Antigravity da `/graphify` deb yozing — u sizning fayllaringizni o'qiydi, bilim grafini quradi va siz bilmagan tuzilmani sizga qaytaradi. Kod bazasini tezroq tushuning. Arxitektura qarorlari ortidagi "nima uchun" savoliga javob toping. diff --git a/graphify/__main__.py b/graphify/__main__.py index 4b1bfa9bc..82e422e0d 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -1,6 +1,7 @@ """graphify CLI - `graphify install` sets up the Claude Code skill.""" from __future__ import annotations +import errno import functools import json import os @@ -23,30 +24,114 @@ # same override (#1423). from graphify.paths import GRAPHIFY_OUT as _GRAPHIFY_OUT +# Install/uninstall subsystem moved to graphify/install.py; re-exported here so +# `from graphify.__main__ import ` keeps working unchanged. +from graphify.install import ( # noqa: E402,F401 + dispatch_install_cli, + _agents_install, + _agents_platform_install, + _agents_platform_uninstall, + _agents_uninstall, + _always_on, + _amp_install, + _amp_legacy_cleanup, + _amp_uninstall, + _antigravity_finalize, + _antigravity_install, + _antigravity_uninstall, + _canonical_platform, + _claude_pretooluse_hooks, + _copy_skill_file, + _cursor_install, + _cursor_uninstall, + _devin_rules_install, + _devin_rules_uninstall, + _gemini_hook, + _install_claude_hook, + _install_codebuddy_hook, + _install_codex_hook, + _install_gemini_hook, + _install_kilo_plugin, + _install_opencode_plugin, + _install_skill_references, + _kilo_config_path, + _kilo_config_write_path, + _kilo_install, + _kilo_uninstall, + _kilo_uninstall_global, + _kiro_install, + _kiro_uninstall, + _load_json_like, + _packaged_skill_refs_dir, + _platform_skill_destination, + _print_banner, + _print_install_usage, + _print_project_git_add_hint, + _project_install, + _project_scope_root, + _project_uninstall, + _project_uninstall_all, + _refresh_all_version_stamps, + _remove_claude_skill_registration, + _remove_skill_file, + _replace_or_append_section, + _resolve_graphify_exe, + _skill_registration, + _strip_graphify_hook, + _strip_graphify_md_section, + _strip_json_comments, + _uninstall_claude_hook, + _uninstall_codebuddy_hook, + _uninstall_codex_hook, + _uninstall_gemini_hook, + _uninstall_kilo_plugin, + _uninstall_opencode_plugin, + claude_install, + claude_uninstall, + codebuddy_install, + codebuddy_uninstall, + gemini_install, + gemini_uninstall, + install, + uninstall_all, + vscode_install, + vscode_uninstall, + _PLATFORM_ALIASES, + _CLAUDE_MD_MARKER, + _CODEBUDDY_MD_MARKER, + _AGENTS_MD_MARKER, + _GEMINI_MD_MARKER, + _VSCODE_INSTRUCTIONS_MARKER, + _ANTIGRAVITY_RULES_PATH, + _ANTIGRAVITY_WORKFLOW_PATH, + _ANTIGRAVITY_WORKFLOW, + _CURSOR_RULE_PATH, + _CURSOR_RULE, + _DEVIN_RULES_PATH, + _DEVIN_RULES, + _KILO_PLUGIN_JS, + _KILO_PLUGIN_PATH, + _KILO_CONFIG_JSON_PATH, + _KILO_CONFIG_JSONC_PATH, + _OPENCODE_PLUGIN_JS, + _OPENCODE_PLUGIN_PATH, + _OPENCODE_CONFIG_PATH, + _PLATFORM_CONFIG, +) +from graphify.cli import ( # noqa: E402,F401 + dispatch_command, + _StageTimer, + _clone_repo, + _default_graph_path, + _enforce_graph_size_cap_or_exit, + _run_hook_guard, + _SEARCH_NUDGE, + _READ_NUDGE, + _HOOK_SOURCE_EXTS, + _GEMINI_NUDGE_TEXT, +) -@functools.lru_cache(maxsize=None) -def _always_on(basename: str) -> str: - """Read a packaged always-on instruction block from graphify/always_on/. - The six always-on blocks (CLAUDE.md / AGENTS.md / GEMINI.md / VS Code - Copilot instructions / Antigravity rules / Kiro steering) live as committed - markdown next to this module, generated by tools/skillgen from a single - human-edited fragment and guarded against drift by ``skillgen --check``. The - installer injects them verbatim via ``_replace_or_append_section``, so the - bytes here must match the former triple-quoted constant exactly — the - always-on-roundtrip validator proves that. - """ - path = Path(__file__).parent / "always_on" / f"{basename}.md" - try: - return path.read_text(encoding="utf-8") - except OSError as exc: - # Defer to use-time so a missing/corrupt packaged block can't crash module - # import (which would brick every CLI command, not just install). Reached - # only by an install/integration path that actually needs this block. - raise RuntimeError( - f"graphify install is incomplete: missing always-on block '{basename}' " - f"at {path}. Reinstall graphifyy (e.g. `uv tool install --reinstall graphifyy`)." - ) from exc _ALWAYS_ON_ALIASES = { @@ -70,53 +155,10 @@ def __getattr__(name: str) -> str: raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -def _default_graph_path() -> str: - return str(Path(_GRAPHIFY_OUT) / "graph.json") -class _StageTimer: - """Print per-stage wall-clock timings to stderr when --timing is set (#1490). - Monotonic (perf_counter), diagnostic-only: emits ``[graphify timing] : - N.Ns`` after each stage and a final total. Off by default, so normal output is - byte-identical and machine-read stdout is untouched. - """ - def __init__(self, enabled: bool) -> None: - import time as _time - self._now = _time.perf_counter - self.enabled = enabled - self.start = self._now() - self._last = self.start - - def mark(self, stage: str) -> None: - now = self._now() - if self.enabled: - print(f"[graphify timing] {stage}: {now - self._last:.1f}s", file=sys.stderr) - self._last = now - - def total(self) -> None: - if self.enabled: - print(f"[graphify timing] total: {self._now() - self.start:.1f}s", file=sys.stderr) - - -def _enforce_graph_size_cap_or_exit(gp: Path) -> None: - """Reject oversized graph files before parsing (CLI exit-on-fail flavor). - - Delegates to ``graphify.security.check_graph_file_size_cap`` and turns the - raised ``ValueError`` into a CLI-style ``error: ...`` message + exit 1. - Use this from ``__main__.py`` subcommands that already use the ``print + - sys.exit(1)`` idiom. Library/MCP/loader callers (``serve._load_graph``, - ``build``, ``benchmark``, ``tree_html``, ``callflow_html``, ``prs``, - ``global_graph``, ``watch``, ``export``) call the security helper directly - and let the ``ValueError`` propagate. - """ - from graphify.security import check_graph_file_size_cap - try: - check_graph_file_size_cap(gp) - except ValueError as exc: - print(f"error: {exc}", file=sys.stderr) - sys.exit(1) def _check_skill_version(skill_dst: Path) -> None: @@ -148,644 +190,91 @@ def _check_skill_version(skill_dst: Path) -> None: except OSError: return if installed != __version__: - print(f" warning: skill is from graphify {installed}, package is {__version__}. Run 'graphify install' to update.", file=sys.stderr) + if _version_tuple(installed) > _version_tuple(__version__): + # The skill on disk is NEWER than the running package. `graphify install` + # writes the package's OWN (older) bundled skill and re-stamps the version, + # so following the old "run install" advice would silently DOWNGRADE the + # skill. The real fix is to upgrade the package (#1568). Common for a stale + # `uv tool` CLI, or a contributor whose dev checkout stamped a newer skill. + print( + f" warning: skill is from graphify {installed}, but the package is " + f"{__version__} (older). Upgrade the package " + f"(e.g. 'uv tool upgrade graphifyy' or 'pip install -U graphifyy'); " + f"running 'graphify install' would downgrade the skill.", + file=sys.stderr, + ) + else: + print(f" warning: skill is from graphify {installed}, package is {__version__}. Run 'graphify install' to update.", file=sys.stderr) -def _refresh_all_version_stamps() -> None: - """After a successful install, update .graphify_version in all other known skill dirs. +def _version_tuple(version: str) -> tuple[int, ...]: + """Parse a version string into a comparable integer tuple (``0.9.2`` -> ``(0, 9, 2)``). - Prevents stale-version warnings from platforms that were installed previously - but not explicitly re-installed during this upgrade. + Reads the leading digits of each dot-segment, so pre/post-release suffixes + (``1.0.0rc1``) compare by their numeric core. A non-numeric or empty segment + becomes 0, so a malformed stamp degrades to a conservative comparison rather + than raising. """ - for name in _PLATFORM_CONFIG: - skill_dst = _platform_skill_destination(name) - vf = skill_dst.parent / ".graphify_version" - if skill_dst.exists(): - vf.write_text(__version__, encoding="utf-8") - - -def _platform_skill_destination(platform_name: str, *, project: bool = False, project_dir: Path | None = None) -> Path: - """Return the skill destination for a platform and scope.""" - if platform_name == "gemini": - if project: - return (project_dir or Path(".")) / ".gemini" / "skills" / "graphify" / "SKILL.md" - if platform.system() == "Windows": - return Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md" - return Path.home() / ".gemini" / "skills" / "graphify" / "SKILL.md" - - if platform_name == "opencode": - if project: - return (project_dir or Path(".")) / ".opencode" / "skills" / "graphify" / "SKILL.md" - return Path.home() / ".config" / "opencode" / "skills" / "graphify" / "SKILL.md" - - if platform_name == "hermes": - if project: - return (project_dir or Path(".")) / ".hermes" / "skills" / "graphify" / "SKILL.md" - # On Windows, Hermes scans %LOCALAPPDATA%\hermes\skills, not ~/.hermes (#1403). - if platform.system() == "Windows": - local_appdata = Path(os.environ.get("LOCALAPPDATA") or (Path.home() / "AppData" / "Local")) - return local_appdata / "hermes" / "skills" / "graphify" / "SKILL.md" - return Path.home() / ".hermes" / "skills" / "graphify" / "SKILL.md" - - if platform_name == "devin": - if project: - return (project_dir or Path(".")) / ".devin" / "skills" / "graphify" / "SKILL.md" - return Path.home() / ".config" / "devin" / "skills" / "graphify" / "SKILL.md" - - if platform_name == "amp": - if project: - return (project_dir or Path(".")) / ".agents" / "skills" / "graphify" / "SKILL.md" - return Path.home() / ".config" / "agents" / "skills" / "graphify" / "SKILL.md" - - if platform_name == "agents": - # The generic Agent-Skills target: project ./.agents/skills, global the - # spec's user-global ~/.agents/skills (read by `npx skills` and compliant - # frameworks), NOT amp's ~/.config/agents/skills. - if project: - return (project_dir or Path(".")) / ".agents" / "skills" / "graphify" / "SKILL.md" - return Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md" - - if platform_name in ("antigravity", "antigravity-windows"): - if project: - return (project_dir or Path(".")) / ".agents" / "skills" / "graphify" / "SKILL.md" - # Global Antigravity skill dir (all workspaces): ~/.gemini/config/skills/ - return Path.home() / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md" - - cfg = _PLATFORM_CONFIG[platform_name] - if project: - return (project_dir or Path(".")) / cfg["skill_dst"] - - if platform_name in ("claude", "windows") and os.environ.get("CLAUDE_CONFIG_DIR"): - return Path(os.environ["CLAUDE_CONFIG_DIR"]) / "skills" / "graphify" / "SKILL.md" - return Path.home() / cfg["skill_dst"] - - -def _packaged_skill_refs_dir(platform_name: str) -> Path | None: - """Return the packaged references source dir for a progressive platform, else None. - - A platform opts into progressive disclosure by setting ``skill_refs`` in its - ``_PLATFORM_CONFIG`` entry. The value names a bundle under - ``graphify/skills//references/``. Reuse keys (e.g. trae-cn) point at - their twin's bundle. - - ``gemini`` has no ``_PLATFORM_CONFIG`` entry: it installs claude's - ``skill.md`` body verbatim (see ``_copy_skill_file``). Since that body is the - lean progressive core that links to ``references/``, gemini needs claude's - references/ sidecar too, or its SKILL.md ships with dead pointers. So gemini - resolves to the claude bundle rather than opting out. - - Bundles ship one platform-group at a time. A host whose bundle directory - ``graphify/skills//`` is not in this build has not gone progressive - yet, so this returns None and the host installs today's monolithic SKILL.md - with no references/ sidecar. Only when the bundle directory IS present does - this return the references path; if that directory then lacks its - ``references/`` subdir, ``_copy_skill_file`` hard-fails (a malformed bundle, - the empty-sidecar regression the wheel-content test also guards). - """ - if platform_name == "gemini": - bundle = "claude" - else: - bundle = _PLATFORM_CONFIG[platform_name].get("skill_refs") - if not bundle: - return None - bundle_dir = Path(__file__).parent / "skills" / bundle - if not bundle_dir.is_dir(): - return None - return bundle_dir / "references" - - -def _install_skill_references(skill_dst: Path, refs_src: Path) -> None: - """Atomically install a packaged references/ sidecar next to SKILL.md. - - Stages the packaged dir into ``references.tmp`` (copytree), drops any stale - ``references/`` already on disk, then ``os.replace``-renames the staged dir - into place. The rename is atomic on the same filesystem, so an interrupted - install never leaves a half-written references/ visible to the agent. - """ - refs_dst = skill_dst.parent / "references" - refs_staged = skill_dst.parent / "references.tmp" - if refs_staged.exists(): - shutil.rmtree(refs_staged) - try: - shutil.copytree(refs_src, refs_staged) - if refs_dst.exists(): - shutil.rmtree(refs_dst) - os.replace(refs_staged, refs_dst) - except Exception: - if refs_staged.exists(): - shutil.rmtree(refs_staged, ignore_errors=True) - raise + parts: list[int] = [] + for segment in str(version).split("."): + digits = "" + for ch in segment: + if ch.isdigit(): + digits += ch + else: + break + parts.append(int(digits) if digits else 0) + return tuple(parts) -def _copy_skill_file(platform_name: str, *, project: bool = False, project_dir: Path | None = None) -> Path: - """Copy a packaged skill file and write its version stamp. - For progressive platforms (those with ``skill_refs`` set), the packaged - ``references/`` sidecar is installed alongside SKILL.md and the single - ``.graphify_version`` stamp covers both. For monolith platforms (no - ``skill_refs``), any orphan ``references/`` left by a prior progressive - install is removed so the on-disk layout matches the package. - """ - skill_file = "skill.md" if platform_name == "gemini" else _PLATFORM_CONFIG[platform_name]["skill_file"] - skill_src = Path(__file__).parent / skill_file - if not skill_src.exists(): - print(f"error: {skill_file} not found in package - reinstall graphify", file=sys.stderr) - sys.exit(1) - - refs_src = _packaged_skill_refs_dir(platform_name) - if refs_src is not None and not refs_src.exists(): - # Progressive platform declared a references bundle that is missing from - # the package. Fail loud rather than silently shipping an empty sidecar. - print( - f"error: references for '{platform_name}' not found in package " - f"({refs_src}) - reinstall graphify", - file=sys.stderr, - ) - sys.exit(1) - - skill_dst = _platform_skill_destination(platform_name, project=project, project_dir=project_dir) - skill_dst.parent.mkdir(parents=True, exist_ok=True) - - # Install the references/ sidecar (or clear an orphan one) BEFORE writing - # SKILL.md, so SKILL.md is the last artifact laid down. An install that is - # interrupted partway then leaves no SKILL.md rather than a SKILL.md that - # points at an absent references/ dir. - if refs_src is not None: - _install_skill_references(skill_dst, refs_src) - print(f" references -> {skill_dst.parent / 'references'}") - else: - # Monolith (or progressive-with-no-refs): clear any orphan references/. - orphan_refs = skill_dst.parent / "references" - if orphan_refs.exists(): - shutil.rmtree(orphan_refs) - - # SKILL.md last (crash-safety), via an atomic temp + rename. - tmp_dst = skill_dst.with_suffix(skill_dst.suffix + ".tmp") - try: - shutil.copy(skill_src, tmp_dst) - os.replace(tmp_dst, skill_dst) - except Exception: - try: - tmp_dst.unlink(missing_ok=True) - except OSError: - pass - raise - - (skill_dst.parent / ".graphify_version").write_text(__version__, encoding="utf-8") - print(f" skill installed -> {skill_dst}") - return skill_dst - - -def _remove_skill_file(platform_name: str, *, project: bool = False, project_dir: Path | None = None) -> bool: - """Remove a platform skill file and its version stamp without touching other scopes.""" - skill_dst = _platform_skill_destination(platform_name, project=project, project_dir=project_dir) - removed = False - if skill_dst.exists(): - skill_dst.unlink() - print(f" skill removed -> {skill_dst}") - removed = True - version_file = skill_dst.parent / ".graphify_version" - if version_file.exists(): - version_file.unlink() - removed = True - refs_dir = skill_dst.parent / "references" - if refs_dir.exists(): - shutil.rmtree(refs_dir) - removed = True - for d in (skill_dst.parent, skill_dst.parent.parent, skill_dst.parent.parent.parent): - try: - d.rmdir() - except OSError: - break - return removed - - -def _project_scope_root(path: Path, project_dir: Path) -> Path: - """Return the top-level project artifact for a project-scoped skill path.""" - try: - rel = path.relative_to(project_dir) - except ValueError: - return path - return project_dir / rel.parts[0] if rel.parts else path -def _remove_claude_skill_registration(project_dir: Path) -> None: - """Remove the project-scoped Claude skill registration file/section.""" - claude_md = project_dir / ".claude" / "CLAUDE.md" - if not claude_md.exists(): - return - content = claude_md.read_text(encoding="utf-8") - if "# graphify" not in content: - return - cleaned = re.sub(r"\n*# graphify\n.*?(?=\n# |\Z)", "", content, flags=re.DOTALL).rstrip() - if cleaned: - claude_md.write_text(cleaned + "\n", encoding="utf-8") - print(f" CLAUDE.md -> graphify skill registration removed from {claude_md}") - else: - claude_md.unlink() - print(f" CLAUDE.md -> deleted {claude_md}") - - -def _print_project_git_add_hint(paths: list[Path]) -> None: - unique: list[str] = [] - for path in paths: - text = path.as_posix().rstrip("/") - if path.exists() and path.is_dir(): - text += "/" - if text not in unique: - unique.append(text) - if not unique: - return - print() - print("Project-scoped install. Add to version control:") - print(f" git add {' '.join(unique)}") - -_SETTINGS_HOOK = { - # Claude Code v2.1.117+ removed dedicated Grep/Glob tools; searches now go through Bash. - # We match on Bash and inspect the command string to avoid firing on every shell call. - "matcher": "Bash", - "hooks": [ - { - "type": "command", - "command": ( - "CMD=$(python3 -c \"" - "import json,sys; d=json.load(sys.stdin); " - "print(d.get('tool_input',d).get('command',''))\" 2>/dev/null || true); " - "case \"$CMD\" in " - r"*grep*|*rg\ *|*ripgrep*|*find\ *|*fd\ *|*ack\ *|*ag\ *) " - " [ -f graphify-out/graph.json ] && " - r""" echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"MANDATORY: graphify-out/graph.json exists. You MUST run `graphify query \"\"` before grepping raw files. Only grep after graphify has oriented you, or to modify/debug specific lines."}}' """ - " || true ;; " - "esac" - ), - } - ], -} -_READ_SETTINGS_HOOK = { - # The Bash hook above never sees a file read through the native Read tool or a - # Glob, which is the most common way an agent skips the graph: answering a - # codebase question by Read-ing many source files one by one (issue #1114). - # Match Read|Glob, inspect the target path, and nudge (never block) only for a - # source/doc file outside graphify-out/ when a graph exists. The parser is - # python3 (already a graphify dependency), the shell is POSIX, and every branch - # fails open, so a legitimate read always goes through. Reading the graph's own - # report under graphify-out/ is suppressed so it never starts a feedback loop. - # The extension test compares each value's real trailing extension (segment - # after the last '/' then after the last '.') against exts -- not a substring - # scan, which both missed framework files like .astro and false-matched .json - # against .js (the substring '.js' is inside '.json'). - "matcher": "Read|Glob", - "hooks": [ - { - "type": "command", - "command": ( - "HIT=$(python3 -c \"" - "import json,sys;" - "d=json.load(sys.stdin);" - "t=d.get('tool_input',d);" - "exts=('.py','.js','.ts','.tsx','.jsx','.astro','.vue','.svelte','.go','.rs','.java','.rb','.c','.h','.cpp','.hpp','.cc','.cs','.kt','.swift','.php','.scala','.lua','.sh','.md','.rst','.txt','.mdx');" - "vals=[str(t.get('file_path') or ''),str(t.get('pattern') or ''),str(t.get('path') or '')];" - "j=' '.join(vals).lower().replace(chr(92),'/');" - "tails=[('.'+x.rsplit('.',1)[-1]) for v in vals if v for x in [v.lower().replace(chr(92),'/').rsplit('/',1)[-1]] if '.' in x];" - "sys.stdout.write('1' if 'graphify-out/' not in j and any(tl in exts for tl in tails) else '')\" 2>/dev/null || true); " - "if [ \"$HIT\" = 1 ] && [ -f graphify-out/graph.json ]; then " - r"""echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"MANDATORY: graphify-out/graph.json exists. You MUST run graphify before reading source files. Use: `graphify query \"\"` (scoped subgraph), `graphify explain \"\"`, or `graphify path \"\" \"\"`. Only read raw files after graphify has oriented you, or to modify/debug specific lines. This rule applies to subagents too — include it in every subagent prompt involving code exploration."}}'; """ - "fi || true" - ), - } - ], -} -def _skill_registration(skill_path: str = "~/.claude/skills/graphify/SKILL.md") -> str: - return ( - "\n# graphify\n" - f"- **graphify** (`{skill_path}`) " - "- any input to knowledge graph. Trigger: `/graphify`\n" - "When the user types `/graphify`, use the installed graphify skill " - "or instructions before doing anything else.\n" - ) - - -_PLATFORM_CONFIG: dict[str, dict] = { - "claude": { - "skill_file": "skill.md", - "skill_dst": Path(".claude") / "skills" / "graphify" / "SKILL.md", - "claude_md": True, - "skill_refs": "claude", - }, - "codex": { - "skill_file": "skill-codex.md", - "skill_dst": Path(".codex") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "codex", - }, - "opencode": { - "skill_file": "skill-opencode.md", - "skill_dst": Path(".config") / "opencode" / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "opencode", - }, - "kilo": { - "skill_file": "skill-kilo.md", - "skill_dst": Path(".config") / "kilo" / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "kilo", - }, - "aider": { - # Monolith: aider ships the full SKILL.md inline, no references/ sidecar. - "skill_file": "skill-aider.md", - "skill_dst": Path(".aider") / "graphify" / "SKILL.md", - "claude_md": False, - }, - "copilot": { - "skill_file": "skill-copilot.md", - "skill_dst": Path(".copilot") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "copilot", - }, - "claw": { - "skill_file": "skill-claw.md", - "skill_dst": Path(".openclaw") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "claw", - }, - "droid": { - "skill_file": "skill-droid.md", - "skill_dst": Path(".factory") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "droid", - }, - "trae": { - "skill_file": "skill-trae.md", - "skill_dst": Path(".trae") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "trae", - }, - "trae-cn": { - # Reuses trae's split bundle (same skill body + references). - "skill_file": "skill-trae.md", - "skill_dst": Path(".trae-cn") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "trae", - }, - "hermes": { - # Reuses claw's split bundle. - "skill_file": "skill-claw.md", - "skill_dst": Path(".hermes") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "claw", - }, - "kiro": { - "skill_file": "skill-kiro.md", - "skill_dst": Path(".kiro") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "kiro", - }, - "pi": { - "skill_file": "skill-pi.md", - "skill_dst": Path(".pi") / "agent" / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "pi", - }, - "codebuddy": { - # Reuses claude's split bundle (shares skill.md). - "skill_file": "skill.md", - "skill_dst": Path(".codebuddy") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "claude", - }, - "antigravity": { - # Rides claude's split bundle (shares skill.md). - "skill_file": "skill.md", - "skill_dst": Path(".agents") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "claude", - }, - "antigravity-windows": { - # Rides windows' split bundle. - "skill_file": "skill-windows.md", - "skill_dst": Path(".agents") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "windows", - }, - "windows": { - "skill_file": "skill-windows.md", - "skill_dst": Path(".claude") / "skills" / "graphify" / "SKILL.md", - "claude_md": True, - "skill_refs": "windows", - }, - "kimi": { - # Reuses claude's split bundle (shares skill.md). - "skill_file": "skill.md", - "skill_dst": Path(".kimi") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "claude", - }, - "amp": { - # Amp searches .agents/skills (project) and ~/.config/agents/skills (user), - # not .amp/skills. The user-scope path is set in _platform_skill_destination. - "skill_file": "skill-amp.md", - "skill_dst": Path(".agents") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "amp", - }, - "agents": { - # The generic cross-framework Agent-Skills target. Global: ~/.agents/skills - # (the spec's user-global location, read by `npx skills` and compliant - # frameworks); project: ./.agents/skills. The CLI accepts `skills` as an - # alias (see _canonical_platform). Ships its own rendered bundle. - "skill_file": "skill-agents.md", - "skill_dst": Path(".agents") / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - "skill_refs": "agents", - }, - "devin": { - # Monolith: devin ships the full SKILL.md inline, no references/ sidecar. - "skill_file": "skill-devin.md", - # User scope: ~/.config/devin/skills/graphify/SKILL.md - # Project scope: .devin/skills/graphify/SKILL.md (overridden in _platform_skill_destination) - "skill_dst": Path(".config") / "devin" / "skills" / "graphify" / "SKILL.md", - "claude_md": False, - }, -} -# CLI-only platform aliases, resolved to a real _PLATFORM_CONFIG key before -# dispatch. `skills` is the friendly alias for the generic `agents` platform -# (the Agent-Skills ecosystem calls them "skills"). -_PLATFORM_ALIASES: dict[str, str] = {"skills": "agents"} -def _canonical_platform(platform_name: str) -> str: - """Resolve a CLI platform alias to its real _PLATFORM_CONFIG key.""" - return _PLATFORM_ALIASES.get(platform_name, platform_name) -def _replace_or_append_section(content: str, marker: str, new_section: str) -> str: - """Idempotently update or append a graphify-owned section in shared files. - If ``marker`` is not in ``content``, append ``new_section`` to the end - (with a blank-line separator if there's existing content). - If ``marker`` IS in ``content``, replace the existing section in place. - The section runs from the first line containing ``marker`` to the line - before the next H2 heading (``## `` at line start), or to EOF if no later - H2 exists. This lets older installs receive the updated copy without - users having to uninstall and reinstall — important for the issue #580 - fix where existing report-first text would otherwise silently linger. - """ - if marker not in content: - if content.strip(): - return content.rstrip() + "\n\n" + new_section.lstrip() - return new_section.lstrip() - - lines = content.split("\n") - start = next((i for i, line in enumerate(lines) if marker in line), None) - if start is None: - return content.rstrip() + "\n\n" + new_section.lstrip() - - end = len(lines) - for j in range(start + 1, len(lines)): - if lines[j].startswith("## "): - end = j - break - - head = "\n".join(lines[:start]).rstrip() - tail = "\n".join(lines[end:]).lstrip() - section = new_section.strip() - - parts: list[str] = [] - if head: - parts.append(head) - parts.append(section) - if tail: - parts.append(tail) - out = "\n\n".join(parts) - if not out.endswith("\n"): - out += "\n" - return out - - -def _print_banner() -> None: - """Amber brain banner on graphify install. TTY-only, never raises.""" - if not sys.stdout.isatty(): - return - try: - if sys.platform == "win32": - import ctypes - ctypes.windll.kernel32.SetConsoleMode( - ctypes.windll.kernel32.GetStdHandle(-11), 7 - ) - A = "\033[38;5;214m" - D = "\033[38;5;130m" - R = "\033[0m" - print(f"""{A} - ╭──◉──╮ ╭──◉──╮ - ╱ ◉ ◉ ╲ ╱ ◉ ◉ ╲ -│ ◉─◉─◉ ◉ ◉─◉─◉ │ -│ ◉ ◉ │ ◉ ◉ │ -│ ◉─◉─◉ ◉ ◉─◉─◉ │ - ╲ ◉ ◉ ╱ ╲ ◉ ◉ ╱ - ╰──◉──╯ ╰──◉──╯ - ◉ - - █▀▀ █▀█ ▄▀█ █▀█ █ █ █ █▀▀ █▄█ - █▄█ █▀▄ █▀█ █▀▀ █▀█ █ █▀ █{D} {__version__}{R} -""") - except Exception: - pass -def install(platform: str = "claude", *, project: bool = False, project_dir: Path | None = None) -> None: - _print_banner() - platform = _canonical_platform(platform) - if platform == "gemini": - gemini_install(project_dir=project_dir, project=project) - return - if platform == "cursor": - _cursor_install(Path(".")) - return - # On Windows, antigravity needs the PowerShell skill, not the bash one - if platform == "antigravity" and sys.platform == "win32": - platform = "antigravity-windows" - if platform not in _PLATFORM_CONFIG: - print( - f"error: unknown platform '{platform}'. Choose from: {', '.join(_PLATFORM_CONFIG)}, gemini, cursor", - file=sys.stderr, - ) - sys.exit(1) - cfg = _PLATFORM_CONFIG[platform] - project_dir = project_dir or Path(".") - skill_dst = _copy_skill_file(platform, project=project, project_dir=project_dir) - if platform == "kilo": - # Kilo Code also supports a native /graphify command file. - command_src = Path(__file__).parent / "command-kilo.md" - if not command_src.exists(): - print( - f"error: command-kilo.md not found in package - reinstall graphify", - file=sys.stderr, - ) - sys.exit(1) - command_dst = Path.home() / ".config" / "kilo" / "command" / "graphify.md" - command_dst.parent.mkdir(parents=True, exist_ok=True) - shutil.copy(command_src, command_dst) - print(f" command installed -> {command_dst}") - - if cfg["claude_md"]: - # Register in the matching Claude Code scope. - claude_md = (project_dir / ".claude" / "CLAUDE.md") if project else Path.home() / ".claude" / "CLAUDE.md" - registration = _skill_registration(".claude/skills/graphify/SKILL.md" if project else "~/.claude/skills/graphify/SKILL.md") - if claude_md.exists(): - content = claude_md.read_text(encoding="utf-8") - if "graphify" in content: - print(f" CLAUDE.md -> already registered (no change)") - else: - claude_md.write_text(content.rstrip() + registration, encoding="utf-8") - print(f" CLAUDE.md -> skill registered in {claude_md}") - else: - claude_md.parent.mkdir(parents=True, exist_ok=True) - claude_md.write_text(registration.lstrip(), encoding="utf-8") - print(f" CLAUDE.md -> created at {claude_md}") - - if platform == "codebuddy": - # Register in ~/.codebuddy/CODEBUDDY.md (CodeBuddy only) - codebuddy_md = Path.home() / ".codebuddy" / "CODEBUDDY.md" - registration = _skill_registration("~/.codebuddy/skills/graphify/SKILL.md") - if codebuddy_md.exists(): - content = codebuddy_md.read_text(encoding="utf-8") - if "graphify" in content: - print(f" CODEBUDDY.md -> already registered (no change)") - else: - codebuddy_md.write_text(content.rstrip() + registration, encoding="utf-8") - print(f" CODEBUDDY.md -> skill registered in {codebuddy_md}") - else: - codebuddy_md.parent.mkdir(parents=True, exist_ok=True) - codebuddy_md.write_text(registration.lstrip(), encoding="utf-8") - print(f" CODEBUDDY.md -> created at {codebuddy_md}") - if platform == "opencode": - _install_opencode_plugin(project_dir if project else Path(".")) +# PreToolUse nudge payloads, emitted verbatim by the shell-agnostic +# `graphify hook-guard` subcommand (see _run_hook_guard). The previous hooks +# inlined POSIX bash (case/esac, [ -f ], single-quoted echo) which Windows +# cmd.exe/PowerShell cannot parse, so on Windows the hook failed and the nudge +# silently vanished — users had to invoke /graphify by hand (#522). Moving the +# logic into a Python subcommand invoked via an absolute exe path makes the hook +# parse identically under sh, cmd.exe and PowerShell. Claude Code accepts +# additionalContext on PreToolUse (Codex Desktop does not — that path stays a +# no-op via `hook-check`). Compact separators keep the payload byte-for-byte the +# same JSON the old `echo` emitted. + + +# Source/doc extensions the Read|Glob guard nudges on (verbatim from the old hook). +# The trailing-extension test (real final path segment, then its last '.') means +# '.json' never false-matches '.js', and framework files like '.astro' are kept. + + + + + + + + + + + + - # Refresh version stamps in all other previously-installed skill dirs so - # stale-version warnings don't fire for platforms not explicitly re-installed. - if project: - _print_project_git_add_hint([_project_scope_root(skill_dst, project_dir)]) - else: - _refresh_all_version_stamps() - print() - print("Done. Open your AI coding assistant and type:") - print() - print(" /graphify .") - print() -def _print_install_usage() -> None: - platforms = ", ".join([*_PLATFORM_CONFIG, "gemini", "cursor"]) - print("Usage: graphify install [--project] [--platform P|P]") - print(f"Platforms: {platforms}") # The always-on instruction blocks are packaged markdown under graphify/always_on/, @@ -793,743 +282,89 @@ def _print_install_usage() -> None: # load keeps the install-string / issue-#580 contract byte-for-byte while letting # a human edit one fragment instead of a triple-quoted literal here. -_CLAUDE_MD_MARKER = "## graphify" -_CODEBUDDY_MD_MARKER = "## graphify" # AGENTS.md section for Codex, OpenCode, and OpenClaw. # All three platforms read AGENTS.md in the project root for persistent instructions. -_AGENTS_MD_MARKER = "## graphify" - - -_GEMINI_MD_MARKER = "## graphify" - -_GEMINI_HOOK = { - "matcher": "read_file|list_directory", - "hooks": [ - { - "type": "command", - "command": ( - 'python -c "' - "import sys,pathlib,json;" - "e=pathlib.Path('graphify-out/graph.json').exists();" - "d={'decision':'allow'};" - "e and d.update({'additionalContext':'graphify: knowledge graph at graphify-out/. For focused questions, run `graphify query \"\"` (scoped subgraph, usually much smaller than GRAPH_REPORT.md) instead of grepping raw files. Read GRAPH_REPORT.md only for broad architecture context.'});" - "sys.stdout.write(json.dumps(d))" - '"' - ), - } - ], -} -def gemini_install(project_dir: Path | None = None, *, project: bool = False) -> None: - """Copy skill file, write GEMINI.md section, and install BeforeTool hook.""" - project_dir = project_dir or Path(".") - skill_dst = _copy_skill_file("gemini", project=project, project_dir=project_dir) - target = project_dir / "GEMINI.md" +# Gemini CLI BeforeTool hook nudge text. The hook always returns +# {"decision":"allow"} (never blocks a tool) and appends this as additionalContext +# when a graph exists. Emitted by `graphify hook-guard gemini`. The old hook was a +# `python -c "..."` one-liner that depended on a bare `python` on PATH (often +# `python`/`py` or absent on Windows) and embedded backticks + escaped quotes that +# Windows PowerShell mangles (#522 follow-up); the subcommand form has no such +# dependency and parses under every shell. + + + + + + + + + - if target.exists(): - content = target.read_text(encoding="utf-8") - new_content = _replace_or_append_section( - content, _GEMINI_MD_MARKER, _always_on("gemini-md") - ) - else: - new_content = _always_on("gemini-md") - - if target.exists() and new_content == target.read_text(encoding="utf-8"): - print(f"graphify already configured in {target.resolve()} (no change)") - else: - target.write_text(new_content, encoding="utf-8") - print(f"graphify section written to {target.resolve()}") - - # Always re-install the Gemini hook so an older payload (e.g. pre-issue-#580 - # wording) is replaced on upgrade. - _install_gemini_hook(project_dir) - if project: - _print_project_git_add_hint([_project_scope_root(skill_dst, project_dir), project_dir / "GEMINI.md", project_dir / ".gemini"]) - print() - print("Gemini CLI will now check the knowledge graph before answering") - print("codebase questions and rebuild it after code changes.") - - -def _install_gemini_hook(project_dir: Path) -> None: - settings_path = project_dir / ".gemini" / "settings.json" - settings_path.parent.mkdir(parents=True, exist_ok=True) - try: - settings = ( - json.loads(settings_path.read_text(encoding="utf-8")) - if settings_path.exists() - else {} - ) - except json.JSONDecodeError: - settings = {} - before_tool = settings.setdefault("hooks", {}).setdefault("BeforeTool", []) - settings["hooks"]["BeforeTool"] = [ - h for h in before_tool if "graphify" not in str(h) - ] - settings["hooks"]["BeforeTool"].append(_GEMINI_HOOK) - settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8") - print(" .gemini/settings.json -> BeforeTool hook registered") - - -def _uninstall_gemini_hook(project_dir: Path) -> None: - settings_path = project_dir / ".gemini" / "settings.json" - if not settings_path.exists(): - return - try: - settings = json.loads(settings_path.read_text(encoding="utf-8")) - except json.JSONDecodeError: - return - before_tool = settings.get("hooks", {}).get("BeforeTool", []) - filtered = [h for h in before_tool if "graphify" not in str(h)] - if len(filtered) == len(before_tool): - return - settings["hooks"]["BeforeTool"] = filtered - settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8") - print(" .gemini/settings.json -> BeforeTool hook removed") -def gemini_uninstall(project_dir: Path | None = None, *, project: bool = False) -> None: - """Remove the graphify section from GEMINI.md, uninstall hook, and remove skill file.""" - project_dir = project_dir or Path(".") - _remove_skill_file("gemini", project=project, project_dir=project_dir) - target = project_dir / "GEMINI.md" - if not target.exists(): - print("No GEMINI.md found in current directory - nothing to do") - return - content = target.read_text(encoding="utf-8") - if _GEMINI_MD_MARKER not in content: - print("graphify section not found in GEMINI.md - nothing to do") - return - cleaned = re.sub( - r"\n*## graphify\n.*?(?=\n## |\Z)", "", content, flags=re.DOTALL - ).rstrip() - if cleaned: - target.write_text(cleaned + "\n", encoding="utf-8") - print(f"graphify section removed from {target.resolve()}") - else: - target.unlink() - print(f"GEMINI.md was empty after removal - deleted {target.resolve()}") - _uninstall_gemini_hook(project_dir) - - -_VSCODE_INSTRUCTIONS_MARKER = "## graphify" - - -def vscode_install(project_dir: Path | None = None) -> None: - """Install graphify skill for VS Code Copilot Chat + write .github/copilot-instructions.md.""" - skill_src = Path(__file__).parent / "skill-vscode.md" - refs_bundle = "vscode" - if not skill_src.exists(): - skill_src = Path(__file__).parent / "skill-copilot.md" - refs_bundle = "copilot" - skill_dst = Path.home() / ".copilot" / "skills" / "graphify" / "SKILL.md" - skill_dst.parent.mkdir(parents=True, exist_ok=True) - tmp_dst = skill_dst.with_suffix(skill_dst.suffix + ".tmp") - try: - shutil.copy(skill_src, tmp_dst) - os.replace(tmp_dst, skill_dst) - except Exception: - try: - tmp_dst.unlink(missing_ok=True) - except OSError: - pass - raise - # Progressive-capable: install the packaged references/ sidecar when present. - refs_src = Path(__file__).parent / "skills" / refs_bundle / "references" - if refs_src.exists(): - _install_skill_references(skill_dst, refs_src) - print(f" references -> {skill_dst.parent / 'references'}") - else: - orphan_refs = skill_dst.parent / "references" - if orphan_refs.exists(): - shutil.rmtree(orphan_refs) - (skill_dst.parent / ".graphify_version").write_text(__version__, encoding="utf-8") - print(f" skill installed -> {skill_dst}") - - instructions = (project_dir or Path(".")) / ".github" / "copilot-instructions.md" - instructions.parent.mkdir(parents=True, exist_ok=True) - if instructions.exists(): - content = instructions.read_text(encoding="utf-8") - new_content = _replace_or_append_section( - content, _VSCODE_INSTRUCTIONS_MARKER, _always_on("vscode-instructions") - ) - if new_content == content: - print(f" {instructions} -> already configured (no change)") - else: - instructions.write_text(new_content, encoding="utf-8") - print(f" {instructions} -> graphify section {'updated' if _VSCODE_INSTRUCTIONS_MARKER in content else 'added'}") - else: - instructions.write_text(_always_on("vscode-instructions"), encoding="utf-8") - print(f" {instructions} -> created") - - print() - print( - "VS Code Copilot Chat configured. Type /graphify in the chat panel to build the graph." - ) - print("Note: for GitHub Copilot CLI (terminal), use: graphify copilot install") - - -def vscode_uninstall(project_dir: Path | None = None) -> None: - """Remove graphify VS Code Copilot Chat skill and .github/copilot-instructions.md section.""" - skill_dst = Path.home() / ".copilot" / "skills" / "graphify" / "SKILL.md" - if skill_dst.exists(): - skill_dst.unlink() - print(f" skill removed -> {skill_dst}") - version_file = skill_dst.parent / ".graphify_version" - if version_file.exists(): - version_file.unlink() - refs_dir = skill_dst.parent / "references" - if refs_dir.exists(): - shutil.rmtree(refs_dir) - for d in ( - skill_dst.parent, - skill_dst.parent.parent, - skill_dst.parent.parent.parent, - ): - try: - d.rmdir() - except OSError: - break - - instructions = (project_dir or Path(".")) / ".github" / "copilot-instructions.md" - if not instructions.exists(): - return - content = instructions.read_text(encoding="utf-8") - if _VSCODE_INSTRUCTIONS_MARKER not in content: - return - cleaned = re.sub( - r"\n*## graphify\n.*?(?=\n## |\Z)", "", content, flags=re.DOTALL - ).rstrip() - if cleaned: - instructions.write_text(cleaned + "\n", encoding="utf-8") - print(f" graphify section removed from {instructions}") - else: - instructions.unlink() - print(f" {instructions} -> deleted (was empty after removal)") -_ANTIGRAVITY_RULES_PATH = Path(".agents") / "rules" / "graphify.md" -_ANTIGRAVITY_WORKFLOW_PATH = Path(".agents") / "workflows" / "graphify.md" -_ANTIGRAVITY_WORKFLOW = """\ ---- -name: graphify -description: Turn any folder of files into a navigable knowledge graph ---- -# Workflow: graphify -Follow the graphify skill installed at ~/.gemini/config/skills/graphify/SKILL.md to run the full pipeline. -If no path argument is given, use `.` (current directory). -""" _KIRO_STEERING_MARKER = "graphify: A knowledge graph of this project" -def _kiro_install(project_dir: Path) -> None: - """Write graphify skill + steering file for Kiro IDE/CLI.""" - project_dir = project_dir or Path(".") - - # Skill file + references/ sidecar + .graphify_version stamp via the shared - # progressive-disclosure helper. Previously this used a bare write_text that - # bypassed _copy_skill_file, so the references/ dir and version stamp were - # never written even though kiro declares skill_refs: "kiro" (#1142). - _copy_skill_file("kiro", project=True, project_dir=project_dir) - - # Steering file → .kiro/steering/graphify.md (always-on) - steering_dir = project_dir / ".kiro" / "steering" - steering_dir.mkdir(parents=True, exist_ok=True) - steering_dst = steering_dir / "graphify.md" - if steering_dst.exists() and steering_dst.read_text(encoding="utf-8") == _always_on("kiro-steering"): - print(f" .kiro/steering/graphify.md -> already configured (no change)") - else: - # File is wholly graphify-owned. Overwrite on upgrade so older - # report-first wording does not silently linger (issue #580). - action = "updated" if steering_dst.exists() else "written" - steering_dst.write_text(_always_on("kiro-steering"), encoding="utf-8") - print(f" .kiro/steering/graphify.md -> always-on steering {action}") - - print() - print("Kiro will now read the knowledge graph before every conversation.") - print("Use /graphify to build or update the graph.") - - -def _kiro_uninstall(project_dir: Path) -> None: - """Remove graphify skill + steering file for Kiro.""" - project_dir = project_dir or Path(".") - removed = [] - - # Skill + .graphify_version + references/ sidecar + empty-dir walk. - skill_dst = _platform_skill_destination("kiro", project=True, project_dir=project_dir) - if _remove_skill_file("kiro", project=True, project_dir=project_dir): - removed.append(str(skill_dst.relative_to(project_dir))) - - steering_dst = project_dir / ".kiro" / "steering" / "graphify.md" - if steering_dst.exists(): - steering_dst.unlink() - removed.append(str(steering_dst.relative_to(project_dir))) - - print("Removed: " + (", ".join(removed) if removed else "nothing to remove")) - - -def _antigravity_finalize(skill_dst: Path, project_dir: Path) -> None: - """Write Antigravity's always-on layer next to an installed skill. - - Injects the native tool-discovery YAML frontmatter into *skill_dst*, then - writes ``.agents/rules/graphify.md`` and ``.agents/workflows/graphify.md`` - under *project_dir*. Shared by the global ``antigravity install`` and the - project-scoped ``install --project --platform antigravity`` paths, so both lay - down the rules/workflows that the uninstall path already expects to remove. - """ - # Inject YAML frontmatter for native Antigravity tool discovery. - if skill_dst.exists(): - content = skill_dst.read_text(encoding="utf-8") - if not content.startswith("---\n"): - frontmatter = "---\nname: graphify-manager\ndescription: Rebuild the code graph or perform manual CLI queries when MCP server is offline.\n---\n\n" - skill_dst.write_text(frontmatter + content, encoding="utf-8") - - # .agents/rules/graphify.md - rules_path = project_dir / _ANTIGRAVITY_RULES_PATH - rules_path.parent.mkdir(parents=True, exist_ok=True) - if rules_path.exists(): - existing = rules_path.read_text(encoding="utf-8") - if _always_on("antigravity-rules").strip() != existing.strip(): - rules_path.write_text(_always_on("antigravity-rules"), encoding="utf-8") - print(f"graphify rule updated at {rules_path.resolve()}") - else: - print(f"graphify rule already configured at {rules_path.resolve()} (no change)") - else: - rules_path.write_text(_always_on("antigravity-rules"), encoding="utf-8") - print(f"graphify rule written to {rules_path.resolve()}") - - # .agents/workflows/graphify.md - wf_path = project_dir / _ANTIGRAVITY_WORKFLOW_PATH - wf_path.parent.mkdir(parents=True, exist_ok=True) - if wf_path.exists(): - existing = wf_path.read_text(encoding="utf-8") - if _ANTIGRAVITY_WORKFLOW.strip() != existing.strip(): - wf_path.write_text(_ANTIGRAVITY_WORKFLOW, encoding="utf-8") - print(f"graphify workflow updated at {wf_path.resolve()}") - else: - print(f"graphify workflow already configured at {wf_path.resolve()} (no change)") - else: - wf_path.write_text(_ANTIGRAVITY_WORKFLOW, encoding="utf-8") - print(f"graphify workflow written to {wf_path.resolve()}") - - -def _antigravity_install(project_dir: Path) -> None: - """Install graphify for Google Antigravity (global skill + .agents/rules + .agents/workflows).""" - # Copy the skill to ~/.gemini/config/skills/graphify/SKILL.md (global), then - # lay down the always-on rules/workflows under the project dir. - install(platform="antigravity") - _antigravity_finalize(_platform_skill_destination("antigravity"), project_dir) - - print() - print("Antigravity will now check the knowledge graph before answering") - print("codebase questions. Run /graphify first to build the graph.") - print() - print( - "To enable full MCP architecture navigation, add this to ~/.gemini/antigravity/mcp_config.json:" - ) - print(' "graphify": {') - print(' "command": "uv",') - print( - ' "args": ["run", "--with", "graphifyy", "--with", "mcp", "-m", "graphify.serve", "${workspace.path}/graphify-out/graph.json"]' - ) - print(" }") - - -def _antigravity_uninstall(project_dir: Path, *, project: bool = False) -> None: - """Remove graphify Antigravity rules, workflow, and skill files.""" - # Remove rules file - rules_path = project_dir / _ANTIGRAVITY_RULES_PATH - if rules_path.exists(): - rules_path.unlink() - print(f"graphify rule removed from {rules_path.resolve()}") - else: - print("No graphify Antigravity rule found - nothing to do") - - # Remove workflow file - wf_path = project_dir / _ANTIGRAVITY_WORKFLOW_PATH - if wf_path.exists(): - wf_path.unlink() - print(f"graphify workflow removed from {wf_path.resolve()}") - - # Remove skill file - skill_dst = _platform_skill_destination("antigravity", project=project, project_dir=project_dir) - if skill_dst.exists(): - skill_dst.unlink() - print(f"graphify skill removed from {skill_dst}") - version_file = skill_dst.parent / ".graphify_version" - if version_file.exists(): - version_file.unlink() - refs_dir = skill_dst.parent / "references" - if refs_dir.exists(): - shutil.rmtree(refs_dir) - for d in ( - skill_dst.parent, - skill_dst.parent.parent, - skill_dst.parent.parent.parent, - ): - try: - d.rmdir() - except OSError: - break - - -_CURSOR_RULE_PATH = Path(".cursor") / "rules" / "graphify.mdc" -_CURSOR_RULE = """\ ---- -description: graphify knowledge graph context -alwaysApply: true ---- - -This project has a graphify knowledge graph at graphify-out/. - -**MANDATORY: Before using Read, Grep, Glob, or Bash to explore the codebase, you MUST run graphify first:** -- `graphify query ""` — scoped subgraph for any codebase or architecture question -- `graphify path "" ""` — dependency path between two symbols -- `graphify explain ""` — all nodes related to a concept - -This applies to YOU and to every subagent you spawn. Include this rule explicitly in every subagent prompt that involves code exploration. Do not skip graphify because files are "already known" or because you are executing a plan — the graph surfaces cross-file dependencies and INFERRED edges that grep and Read cannot find. - -Only use Read/Grep/Glob directly when: -1. graphify has already oriented you and you need to modify or debug specific lines -2. `graphify-out/graph.json` does not exist yet - -- If `graphify-out/wiki/index.md` exists, navigate it instead of reading raw files -- Read `graphify-out/GRAPH_REPORT.md` only for broad architecture review when query/path/explain do not surface enough context -- After modifying code files, run `graphify update .` to keep the graph current (AST-only, no API cost) -""" - - -def _cursor_install(project_dir: Path) -> None: - """Write .cursor/rules/graphify.mdc with alwaysApply: true.""" - rule_path = (project_dir or Path(".")) / _CURSOR_RULE_PATH - rule_path.parent.mkdir(parents=True, exist_ok=True) - if rule_path.exists() and rule_path.read_text(encoding="utf-8") == _CURSOR_RULE: - print(f"graphify rule at {rule_path} already configured (no change)") - return - # File is wholly graphify-owned. Overwrite on upgrade so older - # report-first wording does not silently linger (issue #580). - action = "updated" if rule_path.exists() else "written" - rule_path.write_text(_CURSOR_RULE, encoding="utf-8") - print(f"graphify rule {action} at {rule_path.resolve()}") - print() - print("Cursor will now always include the knowledge graph context.") - print("Run /graphify . first to build the graph if you haven't already.") - - -def _cursor_uninstall(project_dir: Path) -> None: - """Remove .cursor/rules/graphify.mdc.""" - rule_path = (project_dir or Path(".")) / _CURSOR_RULE_PATH - if not rule_path.exists(): - print("No graphify Cursor rule found - nothing to do") - return - rule_path.unlink() - print(f"graphify Cursor rule removed from {rule_path.resolve()}") -# Devin CLI — .windsurf/rules/graphify.md (always-on context) -# Devin reads .windsurf/rules/*.md files the same way Windsurf IDE does. -_DEVIN_RULES_PATH = Path(".windsurf") / "rules" / "graphify.md" -_DEVIN_RULES = """\ -## graphify -This project has a graphify knowledge graph at graphify-out/. -Rules: -- For codebase or architecture questions, when `graphify-out/graph.json` exists, first run `graphify query ""` (or `graphify path "" ""` / `graphify explain ""`). These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output. -- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files -- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context -- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) -""" -def _devin_rules_install(project_dir: Path) -> None: - """Write .windsurf/rules/graphify.md for always-on Devin context.""" - rules_path = (project_dir or Path(".")) / _DEVIN_RULES_PATH - rules_path.parent.mkdir(parents=True, exist_ok=True) - if rules_path.exists() and rules_path.read_text(encoding="utf-8") == _DEVIN_RULES: - print(f" {rules_path} -> already configured (no change)") - return - action = "updated" if rules_path.exists() else "written" - rules_path.write_text(_DEVIN_RULES, encoding="utf-8") - print(f" rules {action} -> {rules_path}") -def _devin_rules_uninstall(project_dir: Path) -> None: - """Remove .windsurf/rules/graphify.md.""" - rules_path = (project_dir or Path(".")) / _DEVIN_RULES_PATH - if not rules_path.exists(): - return - rules_path.unlink() - print(f" rules removed -> {rules_path}") - - -_KILO_PLUGIN_JS = """\ -// graphify Kilo plugin -// Injects a knowledge graph reminder before bash tool calls when the graph exists. -import { existsSync } from "fs"; -import { join } from "path"; - -export const GraphifyPlugin = async ({ directory }) => { - let reminded = false; - - return { - "tool.execute.before": async (input, output) => { - if (reminded) return; - if (!existsSync(join(directory, "graphify-out", "graph.json"))) return; - - if (input.tool === "bash") { - output.args.command = - 'echo "[graphify] Knowledge graph available. Read graphify-out/GRAPH_REPORT.md for god nodes and architecture context before searching files." && ' + - output.args.command; - reminded = true; - } - }, - }; -}; -""" - -_KILO_PLUGIN_PATH = Path(".kilo") / "plugins" / "graphify.js" -_KILO_CONFIG_JSON_PATH = Path(".kilo") / "kilo.json" -_KILO_CONFIG_JSONC_PATH = Path(".kilo") / "kilo.jsonc" - - -def _strip_json_comments(raw: str) -> str: - """Remove JSONC-style comments while leaving string content intact.""" - result: list[str] = [] - in_string = False - escaped = False - line_comment = False - block_comment = False - i = 0 - - while i < len(raw): - ch = raw[i] - nxt = raw[i + 1] if i + 1 < len(raw) else "" - - if line_comment: - if ch == "\n": - line_comment = False - result.append(ch) - i += 1 - continue - - if block_comment: - if ch == "*" and nxt == "/": - block_comment = False - i += 2 - else: - i += 1 - continue - - if in_string: - result.append(ch) - if escaped: - escaped = False - elif ch == "\\": - escaped = True - elif ch == '"': - in_string = False - i += 1 - continue - - if ch == "/" and nxt == "/": - line_comment = True - i += 2 - continue - if ch == "/" and nxt == "*": - block_comment = True - i += 2 - continue - - result.append(ch) - if ch == '"': - in_string = True - i += 1 - - return re.sub(r",(\s*[}\]])", r"\1", "".join(result)) - - -def _load_json_like(config_file: Path) -> dict: - if not config_file.exists(): - return {} - try: - raw = config_file.read_text(encoding="utf-8") - if config_file.suffix == ".jsonc": - raw = _strip_json_comments(raw) - loaded = json.loads(raw) - except (OSError, json.JSONDecodeError): - return {} - return loaded if isinstance(loaded, dict) else {} - - -def _kilo_config_path(project_dir: Path) -> Path: - kilo_dir = (project_dir or Path(".")) / ".kilo" - json_path = kilo_dir / _KILO_CONFIG_JSON_PATH.name - if json_path.exists(): - return json_path - jsonc_path = kilo_dir / _KILO_CONFIG_JSONC_PATH.name - if jsonc_path.exists(): - return jsonc_path - return json_path - - -def _kilo_config_write_path(project_dir: Path) -> Path: - """Write automated Kilo edits to kilo.json so existing JSONC stays untouched.""" - kilo_dir = (project_dir or Path(".")) / ".kilo" - return kilo_dir / _KILO_CONFIG_JSON_PATH.name - - -def _install_kilo_plugin(project_dir: Path) -> None: - """Write graphify.js plugin and register it without rewriting user JSONC.""" - plugin_file = project_dir / _KILO_PLUGIN_PATH - plugin_file.parent.mkdir(parents=True, exist_ok=True) - plugin_file.write_text(_KILO_PLUGIN_JS, encoding="utf-8") - print(f" {_KILO_PLUGIN_PATH} -> tool.execute.before hook written") - - config_file = _kilo_config_path(project_dir) - write_config_file = _kilo_config_write_path(project_dir) - write_config_file.parent.mkdir(parents=True, exist_ok=True) - config = _load_json_like(config_file) - plugins = config.get("plugin") - if not isinstance(plugins, list): - plugins = [] - config["plugin"] = plugins - entry = plugin_file.resolve().as_uri() - if entry not in plugins: - plugins.append(entry) - write_config_file.write_text(json.dumps(config, indent=2), encoding="utf-8") - print(f" {write_config_file.relative_to(project_dir)} -> plugin registered") - else: - print( - f" {config_file.relative_to(project_dir)} -> plugin already registered (no change)" - ) -def _uninstall_kilo_plugin(project_dir: Path) -> None: - """Remove graphify.js plugin and deregister it without rewriting user JSONC.""" - plugin_file = project_dir / _KILO_PLUGIN_PATH - if plugin_file.exists(): - plugin_file.unlink() - print(f" {_KILO_PLUGIN_PATH} -> removed") - config_file = _kilo_config_path(project_dir) - if not config_file.exists(): - return - write_config_file = _kilo_config_write_path(project_dir) - config = _load_json_like(config_file) - plugins = config.get("plugin", []) - if not isinstance(plugins, list): - plugins = [] - entry = plugin_file.resolve().as_uri() - if entry in plugins: - config["plugin"] = [plugin for plugin in plugins if plugin != entry] - if not config["plugin"]: - config.pop("plugin") - write_config_file.parent.mkdir(parents=True, exist_ok=True) - write_config_file.write_text(json.dumps(config, indent=2), encoding="utf-8") - print( - f" {write_config_file.relative_to(project_dir)} -> plugin deregistered" - ) -# OpenCode tool.execute.before plugin — fires before every tool call. -# Injects a graph reminder into bash command output when graph.json exists. -_OPENCODE_PLUGIN_JS = """\ -// graphify OpenCode plugin -// Injects a knowledge graph reminder before bash tool calls when the graph exists. -// -// IMPORTANT: keep the reminder string free of backticks and $(...) constructs. -// The hook prepends `echo "" && ` to the user's bash command; -// backticks inside the double-quoted echo trigger bash command substitution, -// which both corrupts tool output and silently executes the very graphify -// command we are only suggesting. Plain words render fine in opencode's TUI. -import { existsSync } from "fs"; -import { join } from "path"; - -export const GraphifyPlugin = async ({ directory }) => { - let reminded = false; - - return { - "tool.execute.before": async (input, output) => { - if (reminded) return; - if (!existsSync(join(directory, "graphify-out", "graph.json"))) return; - - if (input.tool === "bash") { - output.args.command = - 'echo "[graphify] knowledge graph at graphify-out/. For focused questions, run graphify query with your question (scoped subgraph, usually much smaller than GRAPH_REPORT.md) instead of grepping raw files. Read GRAPH_REPORT.md only for broad architecture context." && ' + - output.args.command; - reminded = true; - } - }, - }; -}; -""" - -_OPENCODE_PLUGIN_PATH = Path(".opencode") / "plugins" / "graphify.js" -_OPENCODE_CONFIG_PATH = Path(".opencode") / "opencode.json" - - -def _install_opencode_plugin(project_dir: Path) -> None: - """Write graphify.js plugin and register it in opencode.json.""" - plugin_file = project_dir / _OPENCODE_PLUGIN_PATH - plugin_file.parent.mkdir(parents=True, exist_ok=True) - plugin_file.write_text(_OPENCODE_PLUGIN_JS, encoding="utf-8") - print(f" {_OPENCODE_PLUGIN_PATH} -> tool.execute.before hook written") - - config_file = project_dir / _OPENCODE_CONFIG_PATH - if config_file.exists(): - try: - config = json.loads(config_file.read_text(encoding="utf-8")) - except json.JSONDecodeError: - config = {} - else: - config = {} - - plugins = config.setdefault("plugin", []) - entry = _OPENCODE_PLUGIN_PATH.as_posix() - if entry not in plugins: - plugins.append(entry) - config_file.write_text(json.dumps(config, indent=2), encoding="utf-8") - print(f" {_OPENCODE_CONFIG_PATH} -> plugin registered") - else: - print(f" {_OPENCODE_CONFIG_PATH} -> plugin already registered (no change)") - - -def _uninstall_opencode_plugin(project_dir: Path) -> None: - """Remove graphify.js plugin and deregister from opencode.json.""" - plugin_file = project_dir / _OPENCODE_PLUGIN_PATH - if plugin_file.exists(): - plugin_file.unlink() - print(f" {_OPENCODE_PLUGIN_PATH} -> removed") - - config_file = project_dir / _OPENCODE_CONFIG_PATH - if not config_file.exists(): - return - try: - config = json.loads(config_file.read_text(encoding="utf-8")) - except json.JSONDecodeError: - return - plugins = config.get("plugin", []) - entry = _OPENCODE_PLUGIN_PATH.as_posix() - if entry in plugins: - plugins.remove(entry) - if not plugins: - config.pop("plugin") - config_file.write_text(json.dumps(config, indent=2), encoding="utf-8") - print(f" {_OPENCODE_CONFIG_PATH} -> plugin deregistered") + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _CODEX_HOOK = { @@ -1553,648 +388,99 @@ def _uninstall_opencode_plugin(project_dir: Path) -> None: } -def _resolve_graphify_exe() -> str: - """Return the absolute path to the graphify executable. - Falls back to bare 'graphify' if resolution fails. Using an absolute path - ensures the hook works in environments where the venv Scripts/ directory is - not on PATH (e.g. VS Code Codex extension on Windows). - """ - import shutil - found = shutil.which("graphify") - if found: - return found - # Derive from sys.executable: same Scripts/ (Windows) or bin/ (Unix) dir - scripts_dir = Path(sys.executable).parent - for name in ("graphify.exe", "graphify"): - candidate = scripts_dir / name - if candidate.exists(): - return str(candidate) - return "graphify" - - -def _install_codex_hook(project_dir: Path) -> None: - """Add graphify PreToolUse hook to .codex/hooks.json.""" - hooks_path = project_dir / ".codex" / "hooks.json" - hooks_path.parent.mkdir(parents=True, exist_ok=True) - - if hooks_path.exists(): - try: - existing = json.loads(hooks_path.read_text(encoding="utf-8")) - except json.JSONDecodeError: - existing = {} - else: - existing = {} - - graphify_exe = _resolve_graphify_exe() - hook_entry = { - "hooks": { - "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [{"type": "command", "command": f"{graphify_exe} hook-check"}], - } - ] - } - } - pre_tool = existing.setdefault("hooks", {}).setdefault("PreToolUse", []) - existing["hooks"]["PreToolUse"] = [h for h in pre_tool if "graphify" not in str(h)] - existing["hooks"]["PreToolUse"].extend(hook_entry["hooks"]["PreToolUse"]) - hooks_path.write_text(json.dumps(existing, indent=2), encoding="utf-8") - print(f" .codex/hooks.json -> PreToolUse hook registered ({graphify_exe} hook-check)") -def _uninstall_codex_hook(project_dir: Path) -> None: - """Remove graphify PreToolUse hook from .codex/hooks.json.""" - hooks_path = project_dir / ".codex" / "hooks.json" - if not hooks_path.exists(): - return - try: - existing = json.loads(hooks_path.read_text(encoding="utf-8")) - except json.JSONDecodeError: - return - pre_tool = existing.get("hooks", {}).get("PreToolUse", []) - filtered = [h for h in pre_tool if "graphify" not in str(h)] - existing["hooks"]["PreToolUse"] = filtered - hooks_path.write_text(json.dumps(existing, indent=2), encoding="utf-8") - print(f" .codex/hooks.json -> PreToolUse hook removed") -def _agents_install(project_dir: Path, platform: str) -> None: - """Write the graphify section to the local AGENTS.md for always-on platforms.""" - target = (project_dir or Path(".")) / "AGENTS.md" - if target.exists(): - content = target.read_text(encoding="utf-8") - new_content = _replace_or_append_section( - content, _AGENTS_MD_MARKER, _always_on("agents-md") - ) - else: - new_content = _always_on("agents-md") - - if target.exists() and new_content == target.read_text(encoding="utf-8"): - print(f"graphify already configured in {target.resolve()} (no change)") - else: - target.write_text(new_content, encoding="utf-8") - print(f"graphify section written to {target.resolve()}") - - if platform == "codex": - _install_codex_hook(project_dir or Path(".")) - elif platform == "opencode": - _install_opencode_plugin(project_dir or Path(".")) - elif platform == "kilo": - _install_kilo_plugin(project_dir or Path(".")) - - print() - print( - f"{platform.capitalize()} will now check the knowledge graph before answering" - ) - print("codebase questions and rebuild it after code changes.") - if platform not in ("codex", "opencode", "kilo"): - print() - print("Note: unlike Claude Code, there is no PreToolUse hook equivalent for") - print( - f"{platform.capitalize()} — the AGENTS.md rules are the always-on mechanism." - ) -def _amp_legacy_cleanup() -> None: - """Best-effort removal of the pre-fix ~/.amp/skills/graphify install dir. - Older graphify versions wrote the Amp skill to ~/.amp/skills, which Amp does - not search. Clean it up on install so a stale, never-loaded copy does not - linger. Failures are ignored (the new path is what matters). - """ - legacy = Path.home() / ".amp" / "skills" / "graphify" - if legacy.exists(): - shutil.rmtree(legacy, ignore_errors=True) - if not legacy.exists(): - print(f" legacy removed -> {legacy}") -def _amp_install(project_dir: Path | None = None) -> None: - """User-scope Amp install: skill into ~/.config/agents/skills + AGENTS.md.""" - _amp_legacy_cleanup() - _copy_skill_file("amp") - _agents_install(project_dir or Path("."), "amp") -def _amp_uninstall(project_dir: Path | None = None) -> None: - """User-scope Amp uninstall: remove the skill and the AGENTS.md section.""" - removed = _remove_skill_file("amp") - if removed: - print("skill removed") - _agents_uninstall(project_dir or Path("."), platform="amp") -def _agents_platform_install(project_dir: Path | None = None) -> None: - """`graphify agents install`: skill into ~/.agents/skills + AGENTS.md. - The amp-twin of the generic Agent-Skills target. Mirrors _amp_install but - lands the skill at the spec's user-global ~/.agents/skills (set in - _platform_skill_destination). Wiring AGENTS.md keeps it honest with the - rendered hooks reference, which points at `graphify agents install`. The bare - `graphify install --platform agents` path stays skill-only (via install()), - exactly as amp's `--platform amp` does. - """ - _copy_skill_file("agents") - _agents_install(project_dir or Path("."), "agents") - - -def _agents_platform_uninstall(project_dir: Path | None = None) -> None: - """`graphify agents uninstall`: remove the skill and the AGENTS.md section.""" - removed = _remove_skill_file("agents") - if removed: - print("skill removed") - _agents_uninstall(project_dir or Path("."), platform="agents") - - -def _project_install(platform_name: str, project_dir: Path | None = None) -> None: - """Install platform skill/config files in the current project.""" - project_dir = project_dir or Path(".") - platform_name = _canonical_platform(platform_name) - if platform_name in ("claude", "windows"): - install(platform=platform_name, project=True, project_dir=project_dir) - claude_install(project_dir) - _print_project_git_add_hint([project_dir / ".claude", project_dir / "CLAUDE.md"]) - elif platform_name == "gemini": - gemini_install(project_dir, project=True) - elif platform_name == "cursor": - _cursor_install(project_dir) - _print_project_git_add_hint([project_dir / ".cursor"]) - elif platform_name == "kiro": - _kiro_install(project_dir) - _print_project_git_add_hint([project_dir / ".kiro"]) - elif platform_name in ("aider", "amp", "codex", "opencode", "claw", "droid", "trae", "trae-cn", "hermes"): - skill_dst = _copy_skill_file(platform_name, project=True, project_dir=project_dir) - _agents_install(project_dir, platform_name) - hint_paths = [_project_scope_root(skill_dst, project_dir), project_dir / "AGENTS.md"] - if platform_name == "opencode": - hint_paths.append(project_dir / ".opencode") - elif platform_name == "codex": - hint_paths.append(project_dir / ".codex") - _print_project_git_add_hint(hint_paths) - elif platform_name == "devin": - skill_dst = _copy_skill_file("devin", project=True, project_dir=project_dir) - _devin_rules_install(project_dir) - _print_project_git_add_hint([_project_scope_root(skill_dst, project_dir), project_dir / ".windsurf"]) - elif platform_name == "antigravity": - # Project-scoped: skill in .agents/skills/ PLUS the .agents/rules + - # .agents/workflows always-on layer (previously this path wrote only the - # skill, leaving the rules/workflows the uninstall path removes unset). - skill_dst = _copy_skill_file("antigravity", project=True, project_dir=project_dir) - _antigravity_finalize(skill_dst, project_dir) - _print_project_git_add_hint([_project_scope_root(skill_dst, project_dir), project_dir / ".agents"]) - elif platform_name in ("copilot", "pi", "kimi", "agents"): - # Skill-only project install: drop SKILL.md (+ references) at the scope - # root. `agents` -> ./.agents/skills/graphify/SKILL.md. - skill_dst = _copy_skill_file(platform_name, project=True, project_dir=project_dir) - _print_project_git_add_hint([_project_scope_root(skill_dst, project_dir)]) - else: - install(platform=platform_name, project=True, project_dir=project_dir) - - -def _project_uninstall(platform_name: str, project_dir: Path | None = None) -> None: - """Remove project-scoped platform skill/config files only.""" - project_dir = project_dir or Path(".") - platform_name = _canonical_platform(platform_name) - if platform_name in ("claude", "windows"): - _remove_skill_file(platform_name, project=True, project_dir=project_dir) - _remove_claude_skill_registration(project_dir) - claude_uninstall(project_dir, project=True) - elif platform_name == "gemini": - gemini_uninstall(project_dir, project=True) - elif platform_name == "cursor": - _cursor_uninstall(project_dir) - elif platform_name == "kiro": - _kiro_uninstall(project_dir) - elif platform_name in ("aider", "amp", "codex", "opencode", "claw", "droid", "trae", "trae-cn", "hermes"): - _remove_skill_file(platform_name, project=True, project_dir=project_dir) - _agents_uninstall(project_dir, platform=platform_name) - if platform_name == "codex": - _uninstall_codex_hook(project_dir) - elif platform_name == "antigravity": - _antigravity_uninstall(project_dir, project=True) - elif platform_name == "devin": - removed = _remove_skill_file("devin", project=True, project_dir=project_dir) - _devin_rules_uninstall(project_dir) - if not removed: - print("nothing to remove") - elif platform_name in ("copilot", "pi", "kimi", "agents"): - removed = _remove_skill_file(platform_name, project=True, project_dir=project_dir) - if not removed: - print("nothing to remove") - elif platform_name == "codebuddy": - codebuddy_uninstall(project_dir) - else: - _remove_skill_file(platform_name, project=True, project_dir=project_dir) - - -def _project_uninstall_all(project_dir: Path | None = None) -> None: - """Remove project-scoped install files without touching user-scope installs.""" - project_dir = project_dir or Path(".") - print("Uninstalling project-scoped graphify files...\n") - for platform_name in _PLATFORM_CONFIG: - _project_uninstall(platform_name, project_dir) - for platform_name in ("gemini", "cursor"): - _project_uninstall(platform_name, project_dir) - print("\nDone.") - - -def _agents_uninstall(project_dir: Path, platform: str = "") -> None: - """Remove the graphify section from the local AGENTS.md.""" - target = (project_dir or Path(".")) / "AGENTS.md" - - if not target.exists(): - print("No AGENTS.md found in current directory - nothing to do") - if platform == "opencode": - _uninstall_opencode_plugin(project_dir or Path(".")) - elif platform == "kilo": - _uninstall_kilo_plugin(project_dir or Path(".")) - return - content = target.read_text(encoding="utf-8") - if _AGENTS_MD_MARKER not in content: - print("graphify section not found in AGENTS.md - nothing to do") - if platform == "opencode": - _uninstall_opencode_plugin(project_dir or Path(".")) - elif platform == "kilo": - _uninstall_kilo_plugin(project_dir or Path(".")) - return - cleaned = re.sub( - r"\n*## graphify\n.*?(?=\n## |\Z)", - "", - content, - flags=re.DOTALL, - ).rstrip() - if cleaned: - target.write_text(cleaned + "\n", encoding="utf-8") - print(f"graphify section removed from {target.resolve()}") - else: - target.unlink() - print(f"AGENTS.md was empty after removal - deleted {target.resolve()}") - - if platform == "opencode": - _uninstall_opencode_plugin(project_dir or Path(".")) - elif platform == "kilo": - _uninstall_kilo_plugin(project_dir or Path(".")) - - -def _kilo_uninstall_global() -> list[str]: - removed = [] - command_dst = Path.home() / ".config" / "kilo" / "command" / "graphify.md" - if command_dst.exists(): - command_dst.unlink() - removed.append(f"command removed: {command_dst}") - try: - command_dst.parent.rmdir() - except OSError: - pass - skill_dst = Path.home() / _PLATFORM_CONFIG["kilo"]["skill_dst"] - if skill_dst.exists(): - skill_dst.unlink() - removed.append(f"skill removed: {skill_dst}") - version_file = skill_dst.parent / ".graphify_version" - if version_file.exists(): - version_file.unlink() - for d in ( - skill_dst.parent, - skill_dst.parent.parent, - skill_dst.parent.parent.parent, - ): - try: - d.rmdir() - except OSError: - break - - return removed - - -def _kilo_install(project_dir: Path) -> None: - """Install native Kilo skill + command globally and always-on project wiring locally.""" - install(platform="kilo") - _agents_install(project_dir or Path("."), "kilo") - - -def _kilo_uninstall(project_dir: Path) -> None: - """Remove Kilo always-on project wiring and global skill/command files.""" - _agents_uninstall(project_dir or Path("."), platform="kilo") - removed = _kilo_uninstall_global() - print("; ".join(removed) if removed else "nothing to remove") - - -def claude_install(project_dir: Path | None = None) -> None: - """Write the graphify section to the local CLAUDE.md.""" - target = (project_dir or Path(".")) / "CLAUDE.md" - - if target.exists(): - content = target.read_text(encoding="utf-8") - new_content = _replace_or_append_section( - content, _CLAUDE_MD_MARKER, _always_on("claude-md") - ) - else: - new_content = _always_on("claude-md") - - if target.exists() and new_content == target.read_text(encoding="utf-8"): - print(f"graphify already configured in {target.resolve()} (no change)") - else: - target.write_text(new_content, encoding="utf-8") - print(f"graphify section written to {target.resolve()}") - - # Always re-install the Claude Code PreToolUse hook so an old hook - # payload (e.g. pre-issue-#580 wording) is replaced on upgrade. - _install_claude_hook(project_dir or Path(".")) - - print() - print("Claude Code will now check the knowledge graph before answering") - print("codebase questions and rebuild it after code changes.") - - -def _install_claude_hook(project_dir: Path) -> None: - """Add graphify PreToolUse hook to .claude/settings.json.""" - settings_path = project_dir / ".claude" / "settings.json" - settings_path.parent.mkdir(parents=True, exist_ok=True) - - if settings_path.exists(): - try: - settings = json.loads(settings_path.read_text(encoding="utf-8")) - except json.JSONDecodeError: - settings = {} - else: - settings = {} - - hooks = settings.setdefault("hooks", {}) - pre_tool = hooks.setdefault("PreToolUse", []) - - hooks["PreToolUse"] = [h for h in pre_tool if not (h.get("matcher") in ("Glob|Grep", "Bash", "Read|Glob") and "graphify" in str(h))] - hooks["PreToolUse"].append(_SETTINGS_HOOK) - hooks["PreToolUse"].append(_READ_SETTINGS_HOOK) - settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8") - print(f" .claude/settings.json -> PreToolUse hooks registered (Bash search + Read/Glob)") - - -def _uninstall_claude_hook(project_dir: Path) -> None: - """Remove graphify PreToolUse hook from .claude/settings.json.""" - settings_path = project_dir / ".claude" / "settings.json" - if not settings_path.exists(): - return - try: - settings = json.loads(settings_path.read_text(encoding="utf-8")) - except json.JSONDecodeError: - return - pre_tool = settings.get("hooks", {}).get("PreToolUse", []) - filtered = [h for h in pre_tool if not (h.get("matcher") in ("Glob|Grep", "Bash", "Read|Glob") and "graphify" in str(h))] - if len(filtered) == len(pre_tool): - return - settings["hooks"]["PreToolUse"] = filtered - settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8") - print(f" .claude/settings.json -> PreToolUse hook removed") - - -def uninstall_all(project_dir: Path | None = None, purge: bool = False) -> None: - """Remove graphify from every platform detected in the current project.""" - pd = project_dir or Path(".") - print("Uninstalling graphify from all detected platforms...\n") - - # Skill-file / config-section uninstallers - claude_uninstall(pd) - codebuddy_uninstall(pd) - gemini_uninstall(pd) - vscode_uninstall(pd) - _cursor_uninstall(pd) - _kiro_uninstall(pd) - _antigravity_uninstall(pd) - # AGENTS.md covers: codex, aider, opencode, claw, droid, trae, trae-cn, hermes, copilot - _agents_uninstall(pd) - # Amp also drops a user-scope skill at ~/.config/agents/skills, which the - # AGENTS.md cleanup above does not touch. - _remove_skill_file("amp") - # The generic agents platform's user-scope skill lives at ~/.agents/skills, - # which neither the AGENTS.md cleanup nor amp's removal reaches. - _remove_skill_file("agents") - _uninstall_opencode_plugin(pd) - _uninstall_codex_hook(pd) - - # Git hook - try: - from graphify.hooks import uninstall as hook_uninstall - result = hook_uninstall(pd) - if result: - print(result) - except Exception: - pass - if purge: - import shutil as _shutil - out = pd / _GRAPHIFY_OUT - if out.exists(): - _shutil.rmtree(out) - print(f"\n {_GRAPHIFY_OUT}/ -> deleted (--purge)") - else: - print(f"\n {_GRAPHIFY_OUT}/ -> not found (nothing to purge)") - print("\nDone. Run 'pip uninstall graphifyy' to remove the package itself.") -def claude_uninstall(project_dir: Path | None = None, *, project: bool = False) -> None: - """Remove the graphify skill tree (SKILL.md + references/) and the CLAUDE.md section. - Mirrors gemini_uninstall: the bare `graphify uninstall` and `graphify claude - uninstall` must remove the installed skill, not just strip CLAUDE.md, or the - progressive-disclosure tree (SKILL.md + references/) is orphaned (#1121). - """ - project_dir = project_dir or Path(".") - _remove_skill_file("claude", project=project, project_dir=project_dir) - target = project_dir / "CLAUDE.md" - if not target.exists(): - print("No CLAUDE.md found in current directory - nothing to do") - return - content = target.read_text(encoding="utf-8") - if _CLAUDE_MD_MARKER not in content: - print("graphify section not found in CLAUDE.md - nothing to do") - return - # Remove the ## graphify section: from the marker to the next ## heading or EOF - cleaned = re.sub( - r"\n*## graphify\n.*?(?=\n## |\Z)", - "", - content, - flags=re.DOTALL, - ).rstrip() - if cleaned: - target.write_text(cleaned + "\n", encoding="utf-8") - print(f"graphify section removed from {target.resolve()}") - else: - target.unlink() - print(f"CLAUDE.md was empty after removal - deleted {target.resolve()}") - - _uninstall_claude_hook(project_dir or Path(".")) - - -def codebuddy_install(project_dir: Path | None = None) -> None: - """Install the graphify skill and CODEBUDDY.md section for CodeBuddy.""" - _copy_skill_file("codebuddy", project=bool(project_dir), project_dir=project_dir) - target = (project_dir or Path(".")) / "CODEBUDDY.md" - - if target.exists(): - content = target.read_text(encoding="utf-8") - new_content = _replace_or_append_section( - content, _CODEBUDDY_MD_MARKER, _always_on("claude-md") - ) - else: - new_content = _always_on("claude-md") - - if target.exists() and new_content == target.read_text(encoding="utf-8"): - print(f"graphify already configured in {target.resolve()} (no change)") - else: - target.write_text(new_content, encoding="utf-8") - print(f"graphify section written to {target.resolve()}") - - # Also write CodeBuddy PreToolUse hook to .codebuddy/settings.json - _install_codebuddy_hook(project_dir or Path(".")) - - print() - print("CodeBuddy will now check the knowledge graph before answering") - print("codebase questions and rebuild it after code changes.") - - -def _install_codebuddy_hook(project_dir: Path) -> None: - """Add graphify PreToolUse hook to .codebuddy/settings.json.""" - settings_path = project_dir / ".codebuddy" / "settings.json" - settings_path.parent.mkdir(parents=True, exist_ok=True) - - if settings_path.exists(): - try: - settings = json.loads(settings_path.read_text(encoding="utf-8")) - except json.JSONDecodeError: - settings = {} - else: - settings = {} - - hooks = settings.setdefault("hooks", {}) - pre_tool = hooks.setdefault("PreToolUse", []) - - hooks["PreToolUse"] = [h for h in pre_tool if not (h.get("matcher") in ("Glob|Grep", "Bash", "Read|Glob") and "graphify" in str(h))] - hooks["PreToolUse"].append(_SETTINGS_HOOK) - hooks["PreToolUse"].append(_READ_SETTINGS_HOOK) - settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8") - print(f" .codebuddy/settings.json -> PreToolUse hooks registered") - - -def _uninstall_codebuddy_hook(project_dir: Path) -> None: - """Remove graphify PreToolUse hook from .codebuddy/settings.json.""" - settings_path = project_dir / ".codebuddy" / "settings.json" - if not settings_path.exists(): - return - try: - settings = json.loads(settings_path.read_text(encoding="utf-8")) - except json.JSONDecodeError: - return - pre_tool = settings.get("hooks", {}).get("PreToolUse", []) - filtered = [h for h in pre_tool if not (h.get("matcher") in ("Glob|Grep", "Bash", "Read|Glob") and "graphify" in str(h))] - if len(filtered) == len(pre_tool): - return - settings["hooks"]["PreToolUse"] = filtered - settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8") - print(f" .codebuddy/settings.json -> PreToolUse hook removed") -def codebuddy_uninstall(project_dir: Path | None = None, *, project: bool = False) -> None: - """Remove the graphify skill tree (SKILL.md + references/) and the CODEBUDDY.md section.""" - project_dir = project_dir or Path(".") - _remove_skill_file("codebuddy", project=project, project_dir=project_dir) - target = project_dir / "CODEBUDDY.md" - if not target.exists(): - print("No CODEBUDDY.md found in current directory - nothing to do") - return - content = target.read_text(encoding="utf-8") - if _CODEBUDDY_MD_MARKER not in content: - print("graphify section not found in CODEBUDDY.md - nothing to do") - return - # Remove the ## graphify section: from the marker to the next ## heading or EOF - cleaned = re.sub( - r"\n*## graphify\n.*?(?=\n## |\Z)", - "", - content, - flags=re.DOTALL, - ).rstrip() - if cleaned: - target.write_text(cleaned + "\n", encoding="utf-8") - print(f"graphify section removed from {target.resolve()}") - else: - target.unlink() - print(f"CODEBUDDY.md was empty after removal - deleted {target.resolve()}") - - _uninstall_codebuddy_hook(project_dir or Path(".")) - -def _clone_repo( - url: str, branch: str | None = None, out_dir: Path | None = None -) -> Path: - """Clone a GitHub repo to a local cache dir and return the path. - - Clones into ~/.graphify/repos// by default so repeated - runs on the same URL reuse the existing clone (git pull instead of clone). - """ - import subprocess as _sp - import re as _re - - # Normalise URL — strip trailing .git if present - url = url.rstrip("/") - if not url.endswith(".git"): - git_url = url + ".git" - else: - git_url = url - url = url[:-4] - - # Extract owner/repo from URL - m = _re.search(r"github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$", url) - if not m: - print(f"error: not a recognised GitHub URL: {url}", file=sys.stderr) - sys.exit(1) - owner, repo = m.group(1), m.group(2) - - if out_dir: - dest = out_dir - else: - dest = Path.home() / ".graphify" / "repos" / owner / repo - - if branch and branch.startswith("-"): - print(f"error: invalid branch name: {branch!r}", file=sys.stderr) - sys.exit(1) - - if dest.exists(): - print(f"Repo already cloned at {dest} - pulling latest...", flush=True) - cmd = ["git", "-C", str(dest), "pull"] - if branch: - cmd += ["origin", "--", branch] - result = _sp.run(cmd, capture_output=True, text=True) - if result.returncode != 0: - print(f"warning: git pull failed:\n{result.stderr}", file=sys.stderr) - else: - dest.parent.mkdir(parents=True, exist_ok=True) - print(f"Cloning {url} -> {dest} ...", flush=True) - cmd = ["git", "clone", "--depth", "1"] - if branch: - cmd += ["--branch", branch] - cmd += ["--", git_url, str(dest)] - result = _sp.run(cmd, capture_output=True, text=True) - if result.returncode != 0: - print(f"error: git clone failed:\n{result.stderr}", file=sys.stderr) - sys.exit(1) - - print(f"Ready at: {dest}", flush=True) - return dest + + + + + + + + + + + + + + + + + + + + + + + + +def _silence_broken_pipe() -> None: + """Handle a downstream reader that closed the pipe early. Redirect stdout to + devnull so the interpreter's shutdown flush does not raise a second time, then + exit 0 — the reader (head, `Select-Object -First N`, `sed q`) has what it needs.""" + try: + devnull = os.open(os.devnull, os.O_WRONLY) + os.dup2(devnull, sys.stdout.fileno()) + except Exception: + pass + sys.exit(0) def main() -> None: + """Console entry point. Wraps the CLI so that when a downstream consumer closes + stdout early, graphify treats it as success instead of crashing with an + unhandled write-to-closed-pipe error and exit 255 — which made CI wrappers and + agent harnesses read a successful query as a command failure (#1807).""" + try: + _run_cli() + # Flush explicitly, inside the guard. Piped stdout is block-buffered, so a + # small fully-buffered output would otherwise only flush at interpreter + # shutdown — outside this try — where a reader that closed the pipe surfaces + # as a noisy "Exception ignored on flushing sys.stdout" and a nonzero exit. + sys.stdout.flush() + except BrokenPipeError: + _silence_broken_pipe() + except OSError as exc: + # Windows surfaces a write to a closed pipe as OSError(EINVAL) rather than + # BrokenPipeError; EPIPE is the POSIX form when it slips past the above. + if getattr(exc, "errno", None) in (errno.EPIPE, errno.EINVAL): + _silence_broken_pipe() + else: + raise + + +def _run_cli() -> None: for _stream in (sys.stdout, sys.stderr): if _stream is not None and hasattr(_stream, "reconfigure"): try: @@ -2205,7 +491,7 @@ def main() -> None: # Skip during install/uninstall (hook writes trigger a fresh check anyway). # Skip during hook-check — it runs on every editor tool use and must be silent. # Deduplicate paths so platforms sharing the same install dir don't warn twice. - _silent_cmds = {"install", "uninstall", "hook-check"} + _silent_cmds = {"install", "uninstall", "hook-check", "hook-guard"} if not any(arg in _silent_cmds for arg in sys.argv): # Resolve each platform's real user-scope destination so per-platform # overrides (gemini, opencode, devin, antigravity, amp) check the dir @@ -2303,9 +589,7 @@ def main() -> None: print(" --top-k-edges N per-symbol outbound edges in inspector (default 12)") print(" --label NAME project label in header") print(" extract headless full extraction (AST + semantic LLM) for CI/scripts") - print(" --backend B ollama|minimax|gemini|kimi|claude|openai|deepseek (default: auto-detect)") - print(" ollama is the local primary; keep OLLAMA_MODEL in the <=8B local safety class") - print(" minimax is a capped dynamic spill/fallback, not the default workhorse") + print(" --backend B gemini|kimi|claude|openai|deepseek|ollama (default: whichever API key is set)") print(" openai also reaches self-hosted OpenAI-compatible servers (llama.cpp,") print(" vLLM, LM Studio): set OPENAI_BASE_URL (e.g. http://localhost:8080/v1)") print(" and OPENAI_MODEL to the model name your server serves") @@ -2313,13 +597,14 @@ def main() -> None: print(" proxy, gateways): set ANTHROPIC_BASE_URL and ANTHROPIC_MODEL") print(" --model M override backend default model") print(" --mode deep aggressive INFERRED-edge semantic extraction") - print(" --max-workers N AST extraction subprocess count (default: half CPUs, capped at 8)") + print(" --max-workers N AST extraction subprocess count (default: cpu_count)") print(" --token-budget N per-chunk token cap for semantic extraction (default: 60000)") print(" --max-concurrency N parallel semantic chunks in flight (default: 4; set 1 for local LLMs)") print(" --api-timeout S per-request timeout in seconds for the LLM client (default: 600)") print(" --out DIR output dir (default: ); writes /graphify-out/") print(" --google-workspace export .gdoc/.gsheet/.gslides shortcuts via gws before extraction") print(" --no-cluster skip clustering, write raw extraction only") + print(" --code-only index code (local AST, no API key) and skip doc/paper/image files") print(" --postgres DSN extract schema from a live PostgreSQL database") print(" maps tables, views, functions + FK relationships;") print(" column-level detail is not represented in the graph") @@ -2415,2662 +700,9 @@ def main() -> None: print(f"Run 'graphify --help' for full usage.") return - if cmd == "install": - # Default to windows platform on Windows, claude elsewhere - default_platform = "windows" if platform.system() == "Windows" else "claude" - selected_platform: str | None = None - project_scope = False - args = sys.argv[2:] - i = 0 - while i < len(args): - arg = args[i] - if arg in ("-h", "--help"): - _print_install_usage() - return - if arg == "--project": - project_scope = True - i += 1 - elif arg.startswith("--platform="): - candidate = arg.split("=", 1)[1] - if selected_platform and selected_platform != candidate: - print("error: specify install platform only once", file=sys.stderr) - sys.exit(1) - selected_platform = candidate - i += 1 - elif arg == "--platform": - if i + 1 >= len(args): - print("error: --platform requires a value", file=sys.stderr) - sys.exit(1) - candidate = args[i + 1] - if selected_platform and selected_platform != candidate: - print("error: specify install platform only once", file=sys.stderr) - sys.exit(1) - selected_platform = candidate - i += 2 - elif arg.startswith("-"): - print(f"error: unknown install option '{arg}'", file=sys.stderr) - sys.exit(1) - else: - if selected_platform and selected_platform != arg: - print("error: specify install platform only once", file=sys.stderr) - sys.exit(1) - selected_platform = arg - i += 1 - chosen_platform = selected_platform or default_platform - if project_scope: - _project_install(chosen_platform, Path(".")) - else: - install(platform=chosen_platform) - elif cmd == "uninstall": - args = sys.argv[2:] - purge = "--purge" in args - project_scope = "--project" in args - selected_platform = None - i = 0 - while i < len(args): - arg = args[i] - if arg in ("--purge", "--project"): - i += 1 - elif arg.startswith("--platform="): - selected_platform = arg.split("=", 1)[1] - i += 1 - elif arg == "--platform": - if i + 1 >= len(args): - print("error: --platform requires a value", file=sys.stderr) - sys.exit(1) - selected_platform = args[i + 1] - i += 2 - elif arg.startswith("-"): - print(f"error: unknown uninstall option '{arg}'", file=sys.stderr) - sys.exit(1) - else: - selected_platform = arg - i += 1 - if project_scope: - if selected_platform: - _project_uninstall(selected_platform, Path(".")) - else: - _project_uninstall_all(Path(".")) - else: - uninstall_all(purge=purge) - elif cmd == "claude": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - if "--project" in sys.argv[3:]: - _project_install("claude", Path(".")) - else: - claude_install() - elif subcmd == "uninstall": - if "--project" in sys.argv[3:]: - _project_uninstall("claude", Path(".")) - else: - claude_uninstall() - else: - print("Usage: graphify claude [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd == "codebuddy": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - codebuddy_install() - elif subcmd == "uninstall": - codebuddy_uninstall() - else: - print("Usage: graphify codebuddy [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd == "gemini": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - gemini_install(project=("--project" in sys.argv[3:])) - elif subcmd == "uninstall": - gemini_uninstall(project=("--project" in sys.argv[3:])) - else: - print("Usage: graphify gemini [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd == "cursor": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - _cursor_install(Path(".")) - elif subcmd == "uninstall": - _cursor_uninstall(Path(".")) - else: - print("Usage: graphify cursor [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd == "vscode": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - vscode_install() - elif subcmd == "uninstall": - vscode_uninstall() - else: - print("Usage: graphify vscode [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd == "copilot": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - if "--project" in sys.argv[3:]: - _project_install("copilot", Path(".")) - else: - install(platform="copilot") - elif subcmd == "uninstall": - if "--project" in sys.argv[3:]: - _project_uninstall("copilot", Path(".")) - else: - removed = _remove_skill_file("copilot") - print("skill removed" if removed else "nothing to remove") - else: - print("Usage: graphify copilot [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd == "kilo": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - _kilo_install(Path(".")) - elif subcmd == "uninstall": - _kilo_uninstall(Path(".")) - else: - print("Usage: graphify kilo [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd == "kiro": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - _kiro_install(Path(".")) - elif subcmd == "uninstall": - _kiro_uninstall(Path(".")) - else: - print("Usage: graphify kiro [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd == "devin": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - if "--project" in sys.argv[3:]: - _project_install("devin", Path(".")) - else: - install(platform="devin") - elif subcmd == "uninstall": - if "--project" in sys.argv[3:]: - _project_uninstall("devin", Path(".")) - else: - removed = _remove_skill_file("devin") - print("skill removed" if removed else "nothing to remove") - else: - print("Usage: graphify devin [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd == "pi": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - if "--project" in sys.argv[3:]: - _project_install("pi", Path(".")) - else: - install("pi") - elif subcmd == "uninstall": - if "--project" in sys.argv[3:]: - _project_uninstall("pi", Path(".")) - else: - _remove_skill_file("pi") - else: - print("Usage: graphify pi [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd == "amp": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - if "--project" in sys.argv[3:]: - _project_install("amp", Path(".")) - else: - _amp_install(Path(".")) - elif subcmd == "uninstall": - if "--project" in sys.argv[3:]: - _project_uninstall("amp", Path(".")) - else: - _amp_uninstall(Path(".")) - else: - print("Usage: graphify amp [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd in ("agents", "skills"): - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - if "--project" in sys.argv[3:]: - _project_install("agents", Path(".")) - else: - _agents_platform_install(Path(".")) - elif subcmd == "uninstall": - if "--project" in sys.argv[3:]: - _project_uninstall("agents", Path(".")) - else: - _agents_platform_uninstall(Path(".")) - else: - print(f"Usage: graphify {cmd} [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd in ("aider", "codex", "opencode", "claw", "droid", "trae", "trae-cn", "hermes"): - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - if "--project" in sys.argv[3:]: - _project_install(cmd, Path(".")) - else: - _agents_install(Path("."), cmd) - elif subcmd == "uninstall": - if "--project" in sys.argv[3:]: - _project_uninstall(cmd, Path(".")) - else: - _agents_uninstall(Path("."), platform=cmd) - if cmd == "codex": - _uninstall_codex_hook(Path(".")) - else: - print(f"Usage: graphify {cmd} [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd == "antigravity": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - if "--project" in sys.argv[3:]: - _project_install("antigravity", Path(".")) - else: - _antigravity_install(Path(".")) - elif subcmd == "uninstall": - if "--project" in sys.argv[3:]: - _project_uninstall("antigravity", Path(".")) - else: - _antigravity_uninstall(Path(".")) - else: - print("Usage: graphify antigravity [install|uninstall]", file=sys.stderr) - sys.exit(1) - elif cmd == "provider": - from graphify.llm import _custom_providers_path, BACKENDS - import json as _json - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - global_path = _custom_providers_path(global_=True) - - if subcmd == "list": - global_path.parent.mkdir(parents=True, exist_ok=True) - existing: dict = {} - if global_path.is_file(): - try: - existing = _json.loads(global_path.read_text(encoding="utf-8")) - except Exception: - pass - if not existing: - print("No custom providers registered.") - else: - for name in existing: - print(f" {name} ({existing[name].get('base_url', '')})") - - elif subcmd == "show": - name = sys.argv[3] if len(sys.argv) > 3 else "" - if not name: - print("Usage: graphify provider show ", file=sys.stderr) - sys.exit(1) - existing = {} - if global_path.is_file(): - try: - existing = _json.loads(global_path.read_text(encoding="utf-8")) - except Exception: - pass - if name not in existing: - print(f"Provider '{name}' not found.", file=sys.stderr) - sys.exit(1) - print(_json.dumps({name: existing[name]}, indent=2)) - - elif subcmd == "add": - args = sys.argv[3:] - name = args[0] if args and not args[0].startswith("-") else "" - if not name: - print("Usage: graphify provider add --base-url URL --default-model MODEL --env-key KEY", file=sys.stderr) - sys.exit(1) - if name in BACKENDS: - print(f"Error: '{name}' is a built-in provider and cannot be overridden.", file=sys.stderr) - sys.exit(1) - base_url = "" - default_model = "" - env_key = "" - pricing_input = 0.0 - pricing_output = 0.0 - i = 1 - while i < len(args): - a = args[i] - if a == "--base-url" and i + 1 < len(args): - base_url = args[i + 1]; i += 2 - elif a.startswith("--base-url="): - base_url = a.split("=", 1)[1]; i += 1 - elif a == "--default-model" and i + 1 < len(args): - default_model = args[i + 1]; i += 2 - elif a.startswith("--default-model="): - default_model = a.split("=", 1)[1]; i += 1 - elif a == "--env-key" and i + 1 < len(args): - env_key = args[i + 1]; i += 2 - elif a.startswith("--env-key="): - env_key = a.split("=", 1)[1]; i += 1 - elif a == "--pricing-input" and i + 1 < len(args): - pricing_input = float(args[i + 1]); i += 2 - elif a == "--pricing-output" and i + 1 < len(args): - pricing_output = float(args[i + 1]); i += 2 - else: - i += 1 - if not base_url or not default_model or not env_key: - print("Error: --base-url, --default-model, and --env-key are required.", file=sys.stderr) - sys.exit(1) - from graphify.llm import provider_base_url_ok - if not provider_base_url_ok(base_url, name): - print(f"Error: refusing to add provider with unsafe base_url {base_url!r}.", file=sys.stderr) - sys.exit(1) - global_path.parent.mkdir(parents=True, exist_ok=True) - existing = {} - if global_path.is_file(): - try: - existing = _json.loads(global_path.read_text(encoding="utf-8")) - except Exception: - pass - existing[name] = { - "base_url": base_url, - "default_model": default_model, - "env_key": env_key, - "pricing": {"input": pricing_input, "output": pricing_output}, - "temperature": 0, - } - global_path.write_text(_json.dumps(existing, indent=2) + "\n", encoding="utf-8") - print(f"Provider '{name}' added. Use with: graphify extract . --backend {name}") - - elif subcmd == "remove": - name = sys.argv[3] if len(sys.argv) > 3 else "" - if not name: - print("Usage: graphify provider remove ", file=sys.stderr) - sys.exit(1) - existing = {} - if global_path.is_file(): - try: - existing = _json.loads(global_path.read_text(encoding="utf-8")) - except Exception: - pass - if name not in existing: - print(f"Provider '{name}' not found.", file=sys.stderr) - sys.exit(1) - del existing[name] - global_path.write_text(_json.dumps(existing, indent=2) + "\n", encoding="utf-8") - print(f"Provider '{name}' removed.") - - else: - print("Usage: graphify provider [add|list|show|remove]", file=sys.stderr) - if subcmd: - sys.exit(1) - elif cmd == "prs": - from graphify.prs import cmd_prs - cmd_prs(sys.argv[2:]) - elif cmd == "hook": - from graphify.hooks import ( - install as hook_install, - uninstall as hook_uninstall, - status as hook_status, - ) - - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd == "install": - print(hook_install(Path("."))) - elif subcmd == "uninstall": - print(hook_uninstall(Path("."))) - elif subcmd == "status": - print(hook_status(Path("."))) - else: - print("Usage: graphify hook [install|uninstall|status]", file=sys.stderr) - sys.exit(1) - elif cmd == "query": - if len(sys.argv) < 3: - print("Usage: graphify query \"\" [--dfs] [--context C] [--budget N] [--graph path]", file=sys.stderr) - sys.exit(1) - from graphify.serve import _query_graph_text - from graphify.security import sanitize_label - from networkx.readwrite import json_graph - from graphify import querylog - - question = sys.argv[2] - use_dfs = "--dfs" in sys.argv - budget = 2000 - graph_path = _default_graph_path() - context_filters: list[str] = [] - args = sys.argv[3:] - i = 0 - while i < len(args): - if args[i] == "--budget" and i + 1 < len(args): - try: - budget = int(args[i + 1]) - except ValueError: - print(f"error: --budget must be an integer", file=sys.stderr) - sys.exit(1) - i += 2 - elif args[i].startswith("--budget="): - try: - budget = int(args[i].split("=", 1)[1]) - except ValueError: - print(f"error: --budget must be an integer", file=sys.stderr) - sys.exit(1) - i += 1 - elif args[i] == "--context" and i + 1 < len(args): - context_filters.append(args[i + 1]) - i += 2 - elif args[i].startswith("--context="): - context_filters.append(args[i].split("=", 1)[1]) - i += 1 - elif args[i] == "--graph" and i + 1 < len(args): - graph_path = args[i + 1] - i += 2 - else: - i += 1 - gp = Path(graph_path).resolve() - if not gp.exists(): - print(f"error: graph file not found: {gp}", file=sys.stderr) - sys.exit(1) - if not gp.suffix == ".json": - print(f"error: graph file must be a .json file", file=sys.stderr) - sys.exit(1) - _enforce_graph_size_cap_or_exit(gp) - try: - import json as _json - import networkx as _nx - - _raw = _json.loads(gp.read_text(encoding="utf-8")) - if "links" not in _raw and "edges" in _raw: - _raw = dict(_raw, links=_raw["edges"]) - try: - G = json_graph.node_link_graph(_raw, edges="links") - except TypeError: - G = json_graph.node_link_graph(_raw) - try: - from graphify.build import graph_has_legacy_ids as _legacy - if _legacy(_raw.get("nodes", [])): - print( - "[graphify] note: this graph uses the pre-#1504 node-ID scheme; " - "rebuild with `graphify extract --force` to get path-qualified IDs " - "(fixes same-name-file collisions).", - file=sys.stderr, - ) - except Exception: - pass - except Exception as exc: - print(f"error: could not load graph: {exc}", file=sys.stderr) - sys.exit(1) - import time as _time - _t0 = _time.perf_counter() - _mode = "dfs" if use_dfs else "bfs" - _result = _query_graph_text( - G, - question, - mode=_mode, - depth=2, - token_budget=budget, - context_filters=context_filters, - ) - querylog.log_query( - kind="query", - question=question, - corpus=str(gp), - result=_result, - mode=_mode, - depth=2, - token_budget=budget, - duration_ms=(_time.perf_counter() - _t0) * 1000, - ) - print(_result) - elif cmd == "affected": - if len(sys.argv) < 3: - print("Usage: graphify affected \"\" [--relation R] [--depth N] [--graph path]", file=sys.stderr) - sys.exit(1) - from graphify.affected import DEFAULT_AFFECTED_RELATIONS, format_affected, load_graph - query = sys.argv[2] - graph_path = _default_graph_path() - depth = 2 - relations: list[str] = [] - args = sys.argv[3:] - i = 0 - while i < len(args): - if args[i] == "--graph" and i + 1 < len(args): - graph_path = args[i + 1] - i += 2 - elif args[i].startswith("--graph="): - graph_path = args[i].split("=", 1)[1] - i += 1 - elif args[i] == "--depth" and i + 1 < len(args): - try: - depth = int(args[i + 1]) - except ValueError: - print("error: --depth must be an integer", file=sys.stderr) - sys.exit(1) - i += 2 - elif args[i].startswith("--depth="): - try: - depth = int(args[i].split("=", 1)[1]) - except ValueError: - print("error: --depth must be an integer", file=sys.stderr) - sys.exit(1) - i += 1 - elif args[i] == "--relation" and i + 1 < len(args): - relations.append(args[i + 1]) - i += 2 - elif args[i].startswith("--relation="): - relations.append(args[i].split("=", 1)[1]) - i += 1 - else: - i += 1 - gp = Path(graph_path).resolve() - if not gp.exists(): - print(f"error: graph file not found: {gp}", file=sys.stderr) - sys.exit(1) - if not gp.suffix == ".json": - print("error: graph file must be a .json file", file=sys.stderr) - sys.exit(1) - try: - graph = load_graph(gp) - except Exception as exc: - print(f"error: could not load graph: {exc}", file=sys.stderr) - sys.exit(1) - print( - format_affected( - graph, - query, - relations=relations or DEFAULT_AFFECTED_RELATIONS, - depth=depth, - ) - ) - elif cmd == "save-result": - # graphify save-result --question Q --answer A [--type T] [--nodes N1 N2 ...] - # [--outcome useful|dead_end|corrected] [--correction TEXT] - import argparse as _ap - - p = _ap.ArgumentParser(prog="graphify save-result") - p.add_argument("--question", required=True) - p.add_argument("--answer", default=None) - p.add_argument("--answer-file", dest="answer_file", default=None) - p.add_argument("--type", dest="query_type", default="query") - p.add_argument("--nodes", nargs="*", default=[]) - p.add_argument("--outcome", choices=("useful", "dead_end", "corrected"), default=None) - p.add_argument("--correction", default=None) - p.add_argument("--memory-dir", default=str(Path(_GRAPHIFY_OUT) / "memory")) - opts = p.parse_args(sys.argv[2:]) - if opts.answer_file: - opts.answer = Path(opts.answer_file).read_text(encoding="utf-8").strip() - elif not opts.answer: - p.error("--answer or --answer-file is required") - from graphify.ingest import save_query_result as _sqr - - out = _sqr( - question=opts.question, - answer=opts.answer, - memory_dir=Path(opts.memory_dir), - query_type=opts.query_type, - source_nodes=opts.nodes or None, - outcome=opts.outcome, - correction=opts.correction, - ) - print(f"Saved to {out}") - elif cmd == "reflect": - import argparse as _ap - - p = _ap.ArgumentParser(prog="graphify reflect") - p.add_argument("--memory-dir", default=str(Path(_GRAPHIFY_OUT) / "memory")) - p.add_argument( - "--out", - default=str(Path(_GRAPHIFY_OUT) / "reflections" / "LESSONS.md"), - ) - p.add_argument("--graph", default=None) - p.add_argument("--analysis", default=None) - p.add_argument("--labels", default=None) - p.add_argument("--half-life-days", type=float, default=30.0, - help="signal weight halves every N days (default 30)") - p.add_argument("--min-corroboration", type=int, default=2, - help="distinct useful results to promote a node to preferred (default 2)") - p.add_argument("--if-stale", action="store_true", - help="skip when LESSONS.md is already newer than every input " - "(e.g. the git hook just refreshed it)") - opts = p.parse_args(sys.argv[2:]) - from graphify.reflect import reflect as _reflect, lessons_fresh as _lessons_fresh - - graph_arg = opts.graph - if graph_arg is None: - default_graph = Path(_GRAPHIFY_OUT) / "graph.json" - if default_graph.exists(): - graph_arg = str(default_graph) - - _gp = Path(graph_arg) if graph_arg else None - _analysis_path = None - _labels_path = None - if _gp is not None: - _analysis_path = Path(opts.analysis) if opts.analysis else ( - _gp.parent / ".graphify_analysis.json") - _labels_path = Path(opts.labels) if opts.labels else ( - _gp.parent / ".graphify_labels.json") - - if opts.if_stale and _lessons_fresh( - Path(opts.out), Path(opts.memory_dir), _gp, _analysis_path, _labels_path - ): - print(f"Lessons already up to date -> {opts.out} (skipped; omit --if-stale to force)") - else: - out_path, agg = _reflect( - memory_dir=Path(opts.memory_dir), - out_path=Path(opts.out), - graph_path=_gp, - analysis_path=_analysis_path, - labels_path=_labels_path, - half_life_days=opts.half_life_days, - min_corroboration=opts.min_corroboration, - ) - c = agg["counts"] - print( - f"Reflected {agg['total']} memories " - f"({c['useful']} useful, {c['dead_end']} dead ends, " - f"{c['corrected']} corrected) -> {out_path}" - ) - elif cmd == "path": - if len(sys.argv) < 4: - print( - 'Usage: graphify path "" "" [--graph path]', - file=sys.stderr, - ) - sys.exit(1) - from graphify.serve import _score_nodes - from networkx.readwrite import json_graph - import networkx as _nx - - source_label = sys.argv[2] - target_label = sys.argv[3] - graph_path = _default_graph_path() - args = sys.argv[4:] - for i, a in enumerate(args): - if a == "--graph" and i + 1 < len(args): - graph_path = args[i + 1] - gp = Path(graph_path).resolve() - if not gp.exists(): - print(f"error: graph file not found: {gp}", file=sys.stderr) - sys.exit(1) - _enforce_graph_size_cap_or_exit(gp) - _raw = json.loads(gp.read_text(encoding="utf-8")) - if "links" not in _raw and "edges" in _raw: - _raw = dict(_raw, links=_raw["edges"]) - # Force directed so the renderer can recover stored caller→callee direction. - _raw = {**_raw, "directed": True} - try: - G = json_graph.node_link_graph(_raw, edges="links") - except TypeError: - G = json_graph.node_link_graph(_raw) - src_scored = _score_nodes(G, [t.lower() for t in source_label.split()]) - tgt_scored = _score_nodes(G, [t.lower() for t in target_label.split()]) - if not src_scored: - print(f"No node matching '{source_label}' found.", file=sys.stderr) - sys.exit(1) - if not tgt_scored: - print(f"No node matching '{target_label}' found.", file=sys.stderr) - sys.exit(1) - src_nid, tgt_nid = src_scored[0][1], tgt_scored[0][1] - # Ambiguity guard: when both queries resolve to the same node, the - # shortest path is trivially zero hops, which is almost never what the - # caller wanted (see bug #828). - if src_nid == tgt_nid: - print( - f"'{source_label}' and '{target_label}' both resolved to the same " - f"node '{src_nid}'. Use a more specific label or the exact node ID.", - file=sys.stderr, - ) - sys.exit(1) - for _name, _scored in (("source", src_scored), ("target", tgt_scored)): - if len(_scored) >= 2: - _top, _runner = _scored[0][0], _scored[1][0] - if _top > 0 and (_top - _runner) / _top < 0.10: - print( - f"warning: {_name} match was ambiguous " - f"(top score {_top:g}, runner-up {_runner:g})", - file=sys.stderr, - ) - try: - path_nodes = _nx.shortest_path(G.to_undirected(as_view=True), src_nid, tgt_nid) - except (_nx.NetworkXNoPath, _nx.NodeNotFound): - print(f"No path found between '{source_label}' and '{target_label}'.") - sys.exit(0) - hops = len(path_nodes) - 1 - segments = [] - from graphify.build import edge_data - for i in range(len(path_nodes) - 1): - u, v = path_nodes[i], path_nodes[i + 1] - # Check which direction the stored edge points. - if G.has_edge(u, v): - edata = edge_data(G, u, v) - forward = True - else: - edata = edge_data(G, v, u) - forward = False - rel = edata.get("relation", "") - conf = edata.get("confidence", "") - conf_str = f" [{conf}]" if conf else "" - if i == 0: - segments.append(G.nodes[u].get("label", u)) - if forward: - segments.append(f"--{rel}{conf_str}--> {G.nodes[v].get('label', v)}") - else: - segments.append(f"<--{rel}{conf_str}-- {G.nodes[v].get('label', v)}") - print(f"Shortest path ({hops} hops):\n " + " ".join(segments)) - from graphify import querylog - querylog.log_query( - kind="path", - question=f"{sys.argv[2]} -> {sys.argv[3]}", - corpus=str(gp), - nodes_returned=hops, - ) - - elif cmd == "explain": - if len(sys.argv) < 3: - print('Usage: graphify explain "" [--graph path]', file=sys.stderr) - sys.exit(1) - from graphify.serve import _find_node - from networkx.readwrite import json_graph - - label = sys.argv[2] - graph_path = _default_graph_path() - args = sys.argv[3:] - for i, a in enumerate(args): - if a == "--graph" and i + 1 < len(args): - graph_path = args[i + 1] - gp = Path(graph_path).resolve() - if not gp.exists(): - print(f"error: graph file not found: {gp}", file=sys.stderr) - sys.exit(1) - _enforce_graph_size_cap_or_exit(gp) - _raw = json.loads(gp.read_text(encoding="utf-8")) - if "links" not in _raw and "edges" in _raw: - _raw = dict(_raw, links=_raw["edges"]) - # Force directed so the renderer can recover stored caller→callee direction. - _raw = {**_raw, "directed": True} - try: - G = json_graph.node_link_graph(_raw, edges="links") - except TypeError: - G = json_graph.node_link_graph(_raw) - matches = _find_node(G, label) - if not matches: - print(f"No node matching '{label}' found.") - sys.exit(0) - nid = matches[0] - d = G.nodes[nid] - print(f"Node: {d.get('label', nid)}") - print(f" ID: {nid}") - print( - f" Source: {d.get('source_file', '')} {d.get('source_location', '')}".rstrip() - ) - print(f" Type: {d.get('file_type', '')}") - print(f" Community: {d.get('community_name') or d.get('community', '')}") - print(f" Degree: {G.degree(nid)}") - from graphify.build import edge_data - connections: list[tuple[str, str, dict]] = [] # (direction, neighbor_id, edge_data) - for nb in G.successors(nid): - connections.append(("out", nb, edge_data(G, nid, nb))) - for nb in G.predecessors(nid): - connections.append(("in", nb, edge_data(G, nb, nid))) - if connections: - print(f"\nConnections ({len(connections)}):") - connections.sort(key=lambda c: G.degree(c[1]), reverse=True) - for direction, nb, edata in connections[:20]: - rel = edata.get("relation", "") - conf = edata.get("confidence", "") - arrow = "-->" if direction == "out" else "<--" - print(f" {arrow} {G.nodes[nb].get('label', nb)} [{rel}] [{conf}]") - if len(connections) > 20: - print(f" ... and {len(connections) - 20} more") - from graphify import querylog - querylog.log_query( - kind="explain", - question=sys.argv[2], - corpus=str(gp), - nodes_returned=len(connections), - ) - - elif cmd == "diagnose": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd != "multigraph": - print( - "Usage: graphify diagnose multigraph " - "[--graph path] [--json] [--max-examples N] " - "[--directed] [--undirected] [--extract-path path]", - file=sys.stderr, - ) - sys.exit(1) - - graph_path = Path(_default_graph_path()) - max_examples = 5 - directed: bool | None = None - direction_flag: str | None = None - json_output = False - extract_path: Path | None = None - - i = 3 - while i < len(sys.argv): - arg = sys.argv[i] - if arg == "--graph": - i += 1 - if i >= len(sys.argv): - print("error: --graph requires a path", file=sys.stderr) - sys.exit(1) - graph_path = Path(sys.argv[i]) - elif arg == "--json": - json_output = True - elif arg == "--max-examples": - i += 1 - if i >= len(sys.argv): - print("error: --max-examples requires an integer", file=sys.stderr) - sys.exit(1) - try: - max_examples = int(sys.argv[i]) - except ValueError: - print("error: --max-examples requires an integer", file=sys.stderr) - sys.exit(1) - if max_examples < 0: - print("error: --max-examples must be >= 0", file=sys.stderr) - sys.exit(1) - elif arg == "--directed": - if direction_flag == "undirected": - print( - "error: --directed and --undirected are mutually exclusive", - file=sys.stderr, - ) - sys.exit(1) - direction_flag = "directed" - directed = True - elif arg == "--undirected": - if direction_flag == "directed": - print( - "error: --directed and --undirected are mutually exclusive", - file=sys.stderr, - ) - sys.exit(1) - direction_flag = "undirected" - directed = False - elif arg == "--extract-path": - i += 1 - if i >= len(sys.argv): - print("error: --extract-path requires a path", file=sys.stderr) - sys.exit(1) - extract_path = Path(sys.argv[i]) - else: - print(f"error: unknown diagnose option {arg}", file=sys.stderr) - sys.exit(1) - i += 1 - - from graphify.diagnostics import ( - diagnose_file, - format_diagnostic_json, - format_diagnostic_report, - ) - - try: - summary = diagnose_file( - graph_path, - directed=directed, - root=Path(".").resolve(), - max_examples=max_examples, - extract_path=extract_path, - ) - except Exception as exc: - print(f"error: {exc}", file=sys.stderr) - sys.exit(1) - - if json_output: - print(json.dumps(format_diagnostic_json(summary), indent=2)) - else: - print(format_diagnostic_report(summary)) - - elif cmd == "add": - if len(sys.argv) < 3: - print( - "Usage: graphify add [--author Name] [--contributor Name] [--dir ./raw]", - file=sys.stderr, - ) - sys.exit(1) - from graphify.ingest import ingest as _ingest - - url = sys.argv[2] - author: str | None = None - contributor: str | None = None - target_dir = Path("raw") - args = sys.argv[3:] - i = 0 - while i < len(args): - if args[i] == "--author" and i + 1 < len(args): - author = args[i + 1] - i += 2 - elif args[i] == "--contributor" and i + 1 < len(args): - contributor = args[i + 1] - i += 2 - elif args[i] == "--dir" and i + 1 < len(args): - target_dir = Path(args[i + 1]) - i += 2 - else: - i += 1 - try: - saved = _ingest(url, target_dir, author=author, contributor=contributor) - print(f"Saved to {saved}") - print("Run /graphify --update in your AI assistant to update the graph.") - except Exception as exc: - print(f"error: {exc}", file=sys.stderr) - sys.exit(1) - - elif cmd == "watch": - watch_path = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(".") - if not watch_path.exists(): - print(f"error: path not found: {watch_path}", file=sys.stderr) - sys.exit(1) - from graphify.watch import watch as _watch - - try: - _watch(watch_path) - except ImportError as exc: - print(f"error: {exc}", file=sys.stderr) - sys.exit(1) - - elif cmd in ("cluster-only", "label"): - # `label` is `cluster-only` that always (re)generates community names with - # the configured backend, even when a .graphify_labels.json already exists. - force_relabel = cmd == "label" - # Mirror the tree/export arg-parsing pattern: walk argv so flags and - # the optional positional path can appear in any order (#724). - no_viz = "--no-viz" in sys.argv - no_label = "--no-label" in sys.argv - missing_only = "--missing-only" in sys.argv - co_timing = "--timing" in sys.argv - _backend_arg = next((a for a in sys.argv if a.startswith("--backend=")), None) - label_backend = _backend_arg.split("=", 1)[1] if _backend_arg else None - _model_arg = next((a for a in sys.argv if a.startswith("--model=")), None) - label_model = _model_arg.split("=", 1)[1] if _model_arg else None - _min_cs_arg = next((a for a in sys.argv if a.startswith("--min-community-size=")), None) - min_community_size = int(_min_cs_arg.split("=")[1]) if _min_cs_arg else 3 - args = sys.argv[2:] - watch_path: Path | None = None - graph_override: Path | None = None - co_resolution: float = 1.0 - co_exclude_hubs: float | None = None - label_max_concurrency: int = 4 - label_batch_size: int = 100 - i_arg = 0 - while i_arg < len(args): - a = args[i_arg] - if a == "--graph" and i_arg + 1 < len(args): - graph_override = Path(args[i_arg + 1]); i_arg += 2 - elif a == "--backend" and i_arg + 1 < len(args): - label_backend = args[i_arg + 1]; i_arg += 2 - elif a.startswith("--backend="): - label_backend = a.split("=", 1)[1]; i_arg += 1 - elif a == "--model" and i_arg + 1 < len(args): - label_model = args[i_arg + 1]; i_arg += 2 - elif a.startswith("--model="): - label_model = a.split("=", 1)[1]; i_arg += 1 - elif a == "--resolution" and i_arg + 1 < len(args): - co_resolution = float(args[i_arg + 1]); i_arg += 2 - elif a.startswith("--resolution="): - co_resolution = float(a.split("=", 1)[1]); i_arg += 1 - elif a == "--exclude-hubs" and i_arg + 1 < len(args): - co_exclude_hubs = float(args[i_arg + 1]); i_arg += 2 - elif a.startswith("--exclude-hubs="): - co_exclude_hubs = float(a.split("=", 1)[1]); i_arg += 1 - elif a == "--max-concurrency" and i_arg + 1 < len(args): - label_max_concurrency = int(args[i_arg + 1]); i_arg += 2 - elif a.startswith("--max-concurrency="): - label_max_concurrency = int(a.split("=", 1)[1]); i_arg += 1 - elif a == "--batch-size" and i_arg + 1 < len(args): - label_batch_size = int(args[i_arg + 1]); i_arg += 2 - elif a.startswith("--batch-size="): - label_batch_size = int(a.split("=", 1)[1]); i_arg += 1 - elif a in ("--no-viz", "--missing-only") or a.startswith("--min-community-size="): - i_arg += 1 - elif a.startswith("--"): - i_arg += 1 - elif watch_path is None: - watch_path = Path(a); i_arg += 1 - else: - i_arg += 1 - if watch_path is None: - watch_path = Path(".") - graph_json = graph_override if graph_override is not None else watch_path / _GRAPHIFY_OUT / "graph.json" - if not graph_json.exists(): - print( - f"error: no graph found at {graph_json} — run /graphify first", - file=sys.stderr, - ) - sys.exit(1) - from networkx.readwrite import json_graph as _jg - from graphify.build import build_from_json - from graphify.cluster import cluster, score_all, remap_communities_to_previous - from graphify.analyze import ( - god_nodes, - surprising_connections, - suggest_questions, - ) - from graphify.report import generate - from graphify.export import to_json, to_html - - stages = _StageTimer(co_timing) - print("Loading existing graph...") - # Solution 3 (#1019): don't hard-exit on an oversized graph.json here. - # Core outputs (graph.json + GRAPH_REPORT.md) still get written; the - # graph.html render below falls back to the community-aggregation view - # (node_limit=5000) when over the cap. - from graphify.security import check_graph_file_size_cap as _check_cap - _over_cap = False - try: - _check_cap(graph_json) - except ValueError: - _over_cap = True - try: - _over_cap_bytes = graph_json.stat().st_size - except OSError: - _over_cap_bytes = -1 - print( - f"warning: graph.json exceeds cap ({_over_cap_bytes} bytes); " - f"falling back to community-aggregation view (node_limit=5000)", - file=sys.stderr, - ) - _raw = json.loads(graph_json.read_text(encoding="utf-8")) - _directed = bool(_raw.get("directed", False)) - G = build_from_json(_raw, directed=_directed) - print(f"Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges") - stages.mark("load") - print("Re-clustering...") - communities = cluster(G, resolution=co_resolution, exclude_hubs_percentile=co_exclude_hubs) - # Mirror the watch/update path (#822): map new cids to prior ones by - # node-overlap so the existing .graphify_labels.json keeps attaching - # to the same conceptual community after re-clustering. Without this, - # labels follow raw cid index and become misaligned whenever the - # graph has changed between labeling and cluster-only (#1027). - previous_node_community = { - n["id"]: n["community"] - for n in _raw.get("nodes", []) - if n.get("community") is not None and n.get("id") is not None - } - if previous_node_community: - communities = remap_communities_to_previous(communities, previous_node_community) - stages.mark("cluster") - cohesion = score_all(G, communities) - gods = god_nodes(G) - surprises = surprising_connections(G, communities) - stages.mark("analyze") - out = watch_path / _GRAPHIFY_OUT - out.mkdir(parents=True, exist_ok=True) - labels_path = out / ".graphify_labels.json" - existing_labels: dict[int, str] = {} - if labels_path.exists(): - try: - existing_labels = { - int(k): v - for k, v in json.loads(labels_path.read_text(encoding="utf-8")).items() - if isinstance(v, str) - } - except Exception: - existing_labels = {} - if labels_path.exists() and not force_relabel: - try: - labels = existing_labels - except Exception: - labels = {cid: f"Community {cid}" for cid in communities} - elif no_label and not force_relabel: - labels = {cid: f"Community {cid}" for cid in communities} - else: - # No labels file yet (or `graphify label` forced a refresh). When run - # standalone there is no orchestrating agent to do skill.md Step 5, so - # auto-name communities with the configured backend rather than leave - # "Community N" (#1097). Degrades to placeholders if no backend/on error. - from graphify.llm import generate_community_labels - print("Labeling communities...") - # The final labels (LLM or placeholder fallback) are persisted to - # .graphify_labels.json by the unconditional write below. - label_communities_input = communities - labels = {} - if missing_only: - labels = { - cid: existing_labels.get(cid, f"Community {cid}") - for cid in communities - } - label_communities_input = { - cid: members - for cid, members in communities.items() - if cid not in existing_labels or existing_labels.get(cid) == f"Community {cid}" - } - generated_labels, _ = generate_community_labels( - G, label_communities_input, backend=label_backend, model=label_model, gods=gods, - max_concurrency=label_max_concurrency, batch_size=label_batch_size, - ) - labels.update(generated_labels) - stages.mark("label") - questions = suggest_questions(G, communities, labels) - tokens = {"input": 0, "output": 0} - from graphify.export import _git_head as _gh - _commit = _gh() - report = generate(G, communities, cohesion, labels, gods, surprises, - {"warning": "cluster-only mode — file stats not available"}, - tokens, str(watch_path), suggested_questions=questions, - min_community_size=min_community_size, built_at_commit=_commit) - (out / "GRAPH_REPORT.md").write_text(report, encoding="utf-8") - stages.mark("report") - from graphify.export import backup_if_protected as _backup - _backup(out) - to_json(G, communities, str(out / "graph.json"), community_labels=labels) - labels_path.write_text(json.dumps({str(k): v for k, v in labels.items()}, ensure_ascii=False), encoding="utf-8") - - # Mirror watch.py pattern: gate to_html so core outputs (graph.json + - # GRAPH_REPORT.md) always land. Honor --no-viz explicitly; otherwise - # fall back to ValueError handling so an oversized graph doesn't crash - # the CLI mid-write and leave a stale graph.html on disk. - html_target = out / "graph.html" - if no_viz: - if html_target.exists(): - html_target.unlink() - stages.mark("export"); stages.total() - print(f"Done - {len(communities)} communities. GRAPH_REPORT.md and graph.json updated (--no-viz; graph.html removed).") - else: - try: - # Over-cap fallback (#1019): force the community-aggregation - # path so an oversized graph still renders a usable graph.html. - _node_limit = 5000 if _over_cap else None - to_html(G, communities, str(html_target), community_labels=labels or None, - node_limit=_node_limit) - stages.mark("export"); stages.total() - print(f"Done - {len(communities)} communities. GRAPH_REPORT.md, graph.json and graph.html updated.") - except ValueError as viz_err: - if html_target.exists(): - html_target.unlink() - print(f"Skipped graph.html: {viz_err}") - stages.mark("export"); stages.total() - print(f"Done - {len(communities)} communities. GRAPH_REPORT.md and graph.json updated.") - - elif cmd == "update": - force = os.environ.get("GRAPHIFY_FORCE", "").lower() in ("1", "true", "yes") - no_cluster = False - args = sys.argv[2:] - watch_arg: str | None = None - for a in args: - if a == "--force": - force = True - continue - if a == "--no-cluster": - no_cluster = True - continue - if a.startswith("-"): - print(f"error: unknown update option: {a}", file=sys.stderr) - sys.exit(2) - if watch_arg is not None: - print("error: update accepts at most one path argument", file=sys.stderr) - sys.exit(2) - watch_arg = a - - if watch_arg is not None: - watch_path = Path(watch_arg) - else: - # Try to recover the scan root saved by the last full build - saved = Path(_GRAPHIFY_OUT) / ".graphify_root" - if saved.exists(): - watch_path = Path(saved.read_text(encoding="utf-8").strip()) - else: - watch_path = Path(".") - if not watch_path.exists(): - print(f"error: path not found: {watch_path}", file=sys.stderr) - sys.exit(1) - from graphify.watch import _rebuild_code - - print(f"Re-extracting code files in {watch_path} (no LLM needed)...") - # Interactive CLI: block on the per-repo lock rather than skip, so the - # user sees their explicit `graphify update` complete instead of - # exiting silently when a hook-driven rebuild happens to be running. - ok = _rebuild_code(watch_path, force=force, no_cluster=no_cluster, block_on_lock=True) - if ok: - print("Code graph updated. For doc/paper/image changes run /graphify --update in your AI assistant.") - if not os.environ.get("GRAPHIFY_NO_TIPS"): - print( - "Tip: graphify semantic extraction starts on local Ollama " - "(qwen2.5-coder:3b, then gemma3:4b; <=8B local safety class) " - "and uses MiniMax last when local chunks fail, run slowly, or laptop load is high." - ) - else: - print( - "Nothing to update or rebuild failed — check output above.", - file=sys.stderr, - ) - sys.exit(1) - - elif cmd == "hook-check": - # Codex Desktop rejects hookSpecificOutput.additionalContext on PreToolUse. - # Keep this as a cross-platform no-op so installed hooks never break Bash - # tool calls. Graph guidance reaches the agent via AGENTS.md / skill instead. - sys.exit(0) - elif cmd == "check-update": - if len(sys.argv) < 3: - print("Usage: graphify check-update ", file=sys.stderr) - sys.exit(1) - from graphify.watch import check_update - - check_update(Path(sys.argv[2]).resolve()) - sys.exit(0) - elif cmd == "tree": - # Emit a D3 v7 collapsible-tree HTML view of graph.json: - # expand-all / collapse-all / reset-view buttons, multi-line - # wrapText labels with separately-coloured name + count, - # depth-based palette, click-to-toggle subtree, hover inspector - # showing top-K outbound edges per symbol. - from typing import Optional as _Opt - from graphify.tree_html import write_tree_html, DEFAULT_MAX_CHILDREN - graph_path = Path(_GRAPHIFY_OUT) / "graph.json" - output_path: "_Opt[Path]" = None - root: "_Opt[str]" = None - max_children = DEFAULT_MAX_CHILDREN - top_k_edges = 0 - project_label: "_Opt[str]" = None - args = sys.argv[2:] - i_arg = 0 - while i_arg < len(args): - a = args[i_arg] - if a == "--graph" and i_arg + 1 < len(args): - graph_path = Path(args[i_arg + 1]); i_arg += 2 - elif a == "--output" and i_arg + 1 < len(args): - output_path = Path(args[i_arg + 1]); i_arg += 2 - elif a == "--root" and i_arg + 1 < len(args): - root = args[i_arg + 1]; i_arg += 2 - elif a == "--max-children" and i_arg + 1 < len(args): - max_children = int(args[i_arg + 1]); i_arg += 2 - elif a == "--top-k-edges" and i_arg + 1 < len(args): - top_k_edges = int(args[i_arg + 1]); i_arg += 2 - elif a == "--label" and i_arg + 1 < len(args): - project_label = args[i_arg + 1]; i_arg += 2 - elif a in ("-h", "--help"): - print("Usage: graphify tree [--graph PATH] [--output HTML]") - print(" --graph PATH path to graph.json (default graphify-out/graph.json)") - print(" --output HTML output path (default graphify-out/GRAPH_TREE.html)") - print(" --root PATH filesystem root (default: longest common dir of all source_files)") - print(" --max-children N cap visible children per node (default 200)") - print(" --top-k-edges N pre-compute top-K outbound edges per symbol (default 12)") - print(" --label NAME project label shown in the page header") - return - else: - i_arg += 1 - if not graph_path.is_file(): - print(f"error: graph.json not found at {graph_path}", file=sys.stderr) - sys.exit(1) - _enforce_graph_size_cap_or_exit(graph_path) - if output_path is None: - output_path = graph_path.parent / "GRAPH_TREE.html" - out = write_tree_html( - graph_path=graph_path, output_path=output_path, - root=root, max_children=max_children, - top_k_edges=top_k_edges, project_label=project_label, - ) - size_kb = out.stat().st_size / 1024 - print(f"wrote {out} ({size_kb:.1f} KB)") - print(f"open with: xdg-open {out} (or file://{out.resolve()})") - sys.exit(0) - - elif cmd == "merge-driver": - # git merge driver for graph.json — takes (base, current, other) and writes - # the union of current+other nodes/edges back to current. Exits 1 on - # corrupt input so git surfaces the conflict instead of silently - # accepting a poisoned merge (see F-005). - # Usage: graphify merge-driver %O %A %B (set in .git/config merge driver) - if len(sys.argv) < 5: - print("Usage: graphify merge-driver ", file=sys.stderr) - sys.exit(1) - _base_path, _current_path, _other_path = sys.argv[2], sys.argv[3], sys.argv[4] - # Hard caps so a malicious or corrupted graph.json cannot exhaust memory - # at parse time. 50 MB / 100k nodes are well above any realistic graph - # (typical graphs are <5 MB / <50k nodes); anything larger should fail - # the merge so a human can investigate. - _MERGE_MAX_BYTES = 50 * 1024 * 1024 - _MERGE_MAX_NODES = 100_000 - import networkx as _nx - from networkx.readwrite import json_graph as _jg - def _load_graph(p: str): - path_obj = Path(p) - try: - size = path_obj.stat().st_size - except OSError as exc: - raise RuntimeError(f"cannot stat {p}: {exc}") from exc - if size > _MERGE_MAX_BYTES: - raise RuntimeError( - f"graph.json {p} is {size} bytes, exceeds {_MERGE_MAX_BYTES}-byte cap" - ) - data = json.loads(path_obj.read_text(encoding="utf-8")) - try: - return _jg.node_link_graph(data, edges="links"), data - except TypeError: - return _jg.node_link_graph(data), data - try: - G_cur, _ = _load_graph(_current_path) - G_oth, _ = _load_graph(_other_path) - except Exception as exc: - print(f"[graphify merge-driver] error loading graphs: {exc}", file=sys.stderr) - sys.exit(1) # surface the conflict so git doesn't accept a corrupt merge - merged = _nx.compose(G_cur, G_oth) - if merged.number_of_nodes() > _MERGE_MAX_NODES: - print( - f"[graphify merge-driver] merged graph has {merged.number_of_nodes()} nodes, " - f"exceeds {_MERGE_MAX_NODES}-node cap; aborting merge.", - file=sys.stderr, - ) - sys.exit(1) - try: - out_data = _jg.node_link_data(merged, edges="links") - except TypeError: - out_data = _jg.node_link_data(merged) - Path(_current_path).write_text(json.dumps(out_data, indent=2), encoding="utf-8") - sys.exit(0) - - elif cmd == "merge-graphs": - # graphify merge-graphs graph1.json graph2.json ... --out merged.json - args = sys.argv[2:] - graph_paths: list[Path] = [] - out_path = Path(_GRAPHIFY_OUT) / "merged-graph.json" - i = 0 - while i < len(args): - if args[i] == "--out" and i + 1 < len(args): - out_path = Path(args[i + 1]) - i += 2 - else: - graph_paths.append(Path(args[i])) - i += 1 - if len(graph_paths) < 2: - print( - "Usage: graphify merge-graphs [...] [--out merged.json]", - file=sys.stderr, - ) - sys.exit(1) - import networkx as _nx - from networkx.readwrite import json_graph as _jg - from graphify.build import prefix_graph_for_global as _prefix - graphs = [] - for gp in graph_paths: - if not gp.exists(): - print(f"error: not found: {gp}", file=sys.stderr) - sys.exit(1) - _enforce_graph_size_cap_or_exit(gp) - data = json.loads(gp.read_text(encoding="utf-8")) - # Normalize edges/links key before loading — graphify writes "links" - # via node_link_data but older runs may have used "edges" (#738). - if "links" not in data and "edges" in data: - data = dict(data, links=data["edges"]) - try: - G = _jg.node_link_graph(data, edges="links") - except TypeError: - G = _jg.node_link_graph(data) - graphs.append(G) - # nx.compose requires all graphs to be the same type. When input graphs - # come from different sources (e.g. an AST-only run vs a full LLM run) one - # may be a MultiGraph and another a Graph. Normalise everything to Graph - # (the graphify default) by converting MultiGraphs with nx.Graph(). - def _to_simple(g: "_nx.Graph") -> "_nx.Graph": - if isinstance(g, _nx.MultiGraph): - return _nx.Graph(g) - return g - merged = _nx.Graph() - for G, gp in zip(graphs, graph_paths): - repo_tag = gp.parent.parent.name # graphify-out/../ → repo dir name - prefixed = _to_simple(_prefix(G, repo_tag)) - merged = _nx.compose(merged, prefixed) - try: - out_data = _jg.node_link_data(merged, edges="links") - except TypeError: - out_data = _jg.node_link_data(merged) - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(json.dumps(out_data, indent=2), encoding="utf-8") - print(f"Merged {len(graphs)} graphs -> {merged.number_of_nodes()} nodes, {merged.number_of_edges()} edges") - print(f"Written to: {out_path}") - - elif cmd == "clone": - if len(sys.argv) < 3: - print( - "Usage: graphify clone [--branch ] [--out ]", - file=sys.stderr, - ) - sys.exit(1) - url = sys.argv[2] - branch: str | None = None - out_dir: Path | None = None - args = sys.argv[3:] - i = 0 - while i < len(args): - if args[i] == "--branch" and i + 1 < len(args): - branch = args[i + 1] - i += 2 - elif args[i] == "--out" and i + 1 < len(args): - out_dir = Path(args[i + 1]) - i += 2 - else: - i += 1 - local_path = _clone_repo(url, branch=branch, out_dir=out_dir) - print(local_path) - - elif cmd == "export": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - if subcmd not in ("html", "callflow-html", "obsidian", "wiki", "svg", "graphml", "neo4j", "falkordb"): - print("Usage: graphify export ", file=sys.stderr) - print(" html [--graph PATH] [--labels PATH] [--node-limit N] [--no-viz]", file=sys.stderr) - print(" callflow-html [GRAPH|DIR] [--graph PATH] [--labels PATH] [--report PATH] [--sections PATH] [--output HTML]", file=sys.stderr) - print(" [--lang auto|zh-CN|en] [--max-sections N] [--diagram-scale N]", file=sys.stderr) - print(" obsidian [--graph PATH] [--labels PATH] [--dir PATH]", file=sys.stderr) - print(" wiki [--graph PATH] [--labels PATH]", file=sys.stderr) - print(" svg [--graph PATH] [--labels PATH]", file=sys.stderr) - print(" graphml [--graph PATH]", file=sys.stderr) - print(" neo4j [--graph PATH] [--push URI] [--user U] [--password P]", file=sys.stderr) - print(" (or set NEO4J_PASSWORD instead of --password to keep it off argv)", file=sys.stderr) - print(" falkordb [--graph PATH] [--push URI] [--user U] [--password P]", file=sys.stderr) - print(" (or set FALKORDB_PASSWORD instead of --password to keep it off argv)", file=sys.stderr) - sys.exit(1) - - # Parse shared args - args = sys.argv[3:] - graph_path = Path(_GRAPHIFY_OUT) / "graph.json" - graph_path_explicit = False - labels_path = Path(_GRAPHIFY_OUT) / ".graphify_labels.json" - labels_path_explicit = False - report_path = Path(_GRAPHIFY_OUT) / "GRAPH_REPORT.md" - report_path_explicit = False - sections_path: Path | None = None - callflow_output: Path | None = None - callflow_lang = "auto" - callflow_max_sections = 15 - callflow_diagram_scale = 1.0 - callflow_max_diagram_nodes = 18 - callflow_max_diagram_edges = 24 - analysis_path = Path(_GRAPHIFY_OUT) / ".graphify_analysis.json" - node_limit = 5000 - no_viz = False - obsidian_dir = Path(_GRAPHIFY_OUT) / "obsidian" - # Shared push-connection settings for the graph-database sinks (neo4j, - # falkordb), parsed from the generic --push/--user/--password flags below. - push_uri: str | None = None - push_user = "neo4j" # Neo4j default user; FalkorDB auth is optional and ignores it - # F-031: prefer an env var so the password never appears on argv (visible - # in `ps` output / shell history). The explicit --password flag still - # overrides it. Each sink reads its own var: FALKORDB_PASSWORD for falkordb, - # NEO4J_PASSWORD otherwise. - push_password: str | None = ( - os.environ.get("FALKORDB_PASSWORD") if subcmd == "falkordb" - else os.environ.get("NEO4J_PASSWORD") - ) or None - i = 0 - while i < len(args): - a = args[i] - if a == "--graph" and i + 1 < len(args): - graph_path = Path(args[i + 1]) - graph_path_explicit = True - i += 2 - elif a == "--labels" and i + 1 < len(args): - labels_path = Path(args[i + 1]) - labels_path_explicit = True - i += 2 - elif a == "--report" and i + 1 < len(args): - report_path = Path(args[i + 1]) - report_path_explicit = True - i += 2 - elif a == "--sections" and i + 1 < len(args): - sections_path = Path(args[i + 1]); i += 2 - elif a == "--output" and i + 1 < len(args): - callflow_output = Path(args[i + 1]).expanduser() - if not callflow_output.is_absolute(): - callflow_output = Path.cwd() / callflow_output - i += 2 - elif a == "--lang" and i + 1 < len(args): - callflow_lang = args[i + 1]; i += 2 - elif a == "--max-sections" and i + 1 < len(args): - callflow_max_sections = int(args[i + 1]); i += 2 - elif a == "--diagram-scale" and i + 1 < len(args): - callflow_diagram_scale = float(args[i + 1]); i += 2 - elif a == "--max-diagram-nodes" and i + 1 < len(args): - callflow_max_diagram_nodes = int(args[i + 1]); i += 2 - elif a == "--max-diagram-edges" and i + 1 < len(args): - callflow_max_diagram_edges = int(args[i + 1]); i += 2 - elif a in ("-h", "--help") and subcmd == "callflow-html": - print("Usage: graphify export callflow-html [GRAPH|DIR] [--graph PATH] [--labels PATH]") - print(" --report PATH path to GRAPH_REPORT.md") - print(" --sections PATH JSON section definitions") - print(" --output HTML output path (default graphify-out/-callflow.html)") - print(" --lang LANG auto, zh-CN, en, etc. (default auto)") - print(" --max-sections N maximum auto-derived sections (default 15)") - print(" --diagram-scale N Mermaid diagram scale (default 1.0)") - print(" --max-diagram-nodes N representative nodes per section (default 18)") - print(" --max-diagram-edges N representative edges per section (default 24)") - sys.exit(0) - elif a == "--node-limit" and i + 1 < len(args): - node_limit = int(args[i + 1]); i += 2 - elif a == "--no-viz": - no_viz = True; i += 1 - elif a == "--dir" and i + 1 < len(args): - obsidian_dir = Path(args[i + 1]); i += 2 - elif a == "--push" and i + 1 < len(args): - push_uri = args[i + 1]; i += 2 - elif a == "--user" and i + 1 < len(args): - push_user = args[i + 1]; i += 2 - elif a == "--password" and i + 1 < len(args): - push_password = args[i + 1]; i += 2 - elif subcmd == "callflow-html" and not a.startswith("-") and not graph_path_explicit: - candidate = Path(a) - if candidate.name == "graph.json" or candidate.suffix.lower() == ".json": - graph_path = candidate - elif (candidate / "graph.json").exists(): - graph_path = candidate / "graph.json" - else: - graph_path = candidate / _GRAPHIFY_OUT / "graph.json" - graph_path_explicit = True - i += 1 - else: - i += 1 - - graph_path = graph_path.expanduser() - if graph_path_explicit: - graph_out_dir = graph_path.parent - if not labels_path_explicit: - labels_path = graph_out_dir / ".graphify_labels.json" - if not report_path_explicit: - report_path = graph_out_dir / "GRAPH_REPORT.md" - labels_path = labels_path.expanduser() - report_path = report_path.expanduser() - - if not graph_path.exists(): - print(f"error: graph not found: {graph_path}. Run /graphify first.", file=sys.stderr) - sys.exit(1) - - if subcmd == "callflow-html": - from graphify.callflow_html import write_callflow_html as _write_callflow_html - out = _write_callflow_html( - graph=graph_path, - report=report_path, - labels=labels_path, - sections=sections_path, - output=callflow_output, - lang=callflow_lang, - max_sections=callflow_max_sections, - diagram_scale=callflow_diagram_scale, - max_diagram_nodes=callflow_max_diagram_nodes, - max_diagram_edges=callflow_max_diagram_edges, - verbose=True, - ) - print(f"callflow HTML written - open in any browser: {out}") - sys.exit(0) - - from networkx.readwrite import json_graph as _jg - from graphify.build import build_from_json as _bfj - from graphify.security import check_graph_file_size_cap as _check_cap - - # Solution 3 (#1019): for the HTML view, an oversized graph.json should - # not be a hard error. Detect the over-cap condition here and fall back - # to the community-aggregation view (node_limit=5000) below instead of - # exiting 1. All other subcommands keep the hard cap. - _over_cap = False - try: - _check_cap(graph_path) - except ValueError as _cap_err: - if subcmd == "html": - _over_cap = True - try: - _over_cap_bytes = graph_path.stat().st_size - except OSError: - _over_cap_bytes = -1 - print( - f"warning: graph.json exceeds cap ({_over_cap_bytes} bytes); " - f"falling back to community-aggregation view (node_limit=5000)", - file=sys.stderr, - ) - else: - print(f"error: {_cap_err}", file=sys.stderr) - sys.exit(1) - _raw = json.loads(graph_path.read_text(encoding="utf-8")) - if "links" not in _raw and "edges" in _raw: - _raw = dict(_raw, links=_raw["edges"]) - try: - G = _jg.node_link_graph(_raw, edges="links") - except TypeError: - G = _jg.node_link_graph(_raw) - - # Load optional analysis/labels - communities: dict[int, list[str]] = {} - if analysis_path.exists(): - _an = json.loads(analysis_path.read_text(encoding="utf-8")) - communities = {int(k): v for k, v in _an.get("communities", {}).items()} - cohesion: dict[int, float] = {int(k): v for k, v in _an.get("cohesion", {}).items()} - gods_data = _an.get("gods", []) - else: - cohesion = {} - gods_data = [] - - # Fallback: graph.json carries the per-node community as a node attribute - # (`to_json` writes it on every node). The analysis sidecar is the - # canonical source — but the post-commit / watch rebuild path doesn't - # regenerate it, and `extract` may have its temp files cleaned up. When - # that happens, `graphify export html` previously bailed with - # "Single community - aggregated view not useful." even though the - # per-node attribute had the right data all along. Reconstruct from - # the graph itself so downstream subcommands (html, obsidian, wiki, - # svg, graphml, neo4j) don't silently produce a degraded artifact. - if not communities: - reconstructed: dict[int, list[str]] = {} - for node_id, data in G.nodes(data=True): - cid_raw = data.get("community") - if cid_raw is None: - continue - try: - cid = int(cid_raw) - except (TypeError, ValueError): - continue - reconstructed.setdefault(cid, []).append(str(node_id)) - if reconstructed: - communities = reconstructed - - labels: dict[int, str] = {} - if labels_path.exists(): - labels = {int(k): v for k, v in json.loads(labels_path.read_text(encoding="utf-8")).items()} - - out_dir = graph_path.parent - - if subcmd == "html": - from graphify.export import to_html as _to_html - if no_viz: - html_target = out_dir / "graph.html" - if html_target.exists(): - html_target.unlink() - print("--no-viz: skipped graph.html") - else: - # Over-cap fallback (#1019): force the community-aggregation - # path so the oversized graph still renders a usable artifact. - _effective_node_limit = 5000 if _over_cap else node_limit - _to_html(G, communities, str(out_dir / "graph.html"), - community_labels=labels or None, node_limit=_effective_node_limit) - if G.number_of_nodes() <= _effective_node_limit: - print(f"graph.html written - open in any browser, no server needed") - if _over_cap: - sys.exit(0) - - elif subcmd == "obsidian": - from graphify.export import to_obsidian as _to_obsidian, to_canvas as _to_canvas - n = _to_obsidian(G, communities, str(obsidian_dir), - community_labels=labels or None, cohesion=cohesion or None) - print(f"Obsidian vault: {n} notes in {obsidian_dir}/") - _to_canvas(G, communities, str(obsidian_dir / "graph.canvas"), - community_labels=labels or None) - print(f"Canvas: {obsidian_dir}/graph.canvas") - print(f"Open {obsidian_dir}/ as a vault in Obsidian.") - - elif subcmd == "wiki": - from graphify.wiki import to_wiki as _to_wiki - from graphify.analyze import god_nodes as _god_nodes - if not communities: - print( - "error: .graphify_analysis.json is missing or empty — refusing to export wiki to prevent data loss.\n" - "Run `graphify extract .` (or `graphify cluster-only .`) to regenerate community data first.", - file=sys.stderr, - ) - sys.exit(1) - if not gods_data: - gods_data = _god_nodes(G) - n = _to_wiki(G, communities, str(out_dir / "wiki"), - community_labels=labels or None, cohesion=cohesion or None, - god_nodes_data=gods_data) - print(f"Wiki: {n} articles written to {out_dir}/wiki/") - print(f" {out_dir}/wiki/index.md -> agent entry point") - - elif subcmd == "svg": - from graphify.export import to_svg as _to_svg - _to_svg(G, communities, str(out_dir / "graph.svg"), - community_labels=labels or None) - print(f"graph.svg written - embeds in Obsidian, Notion, GitHub READMEs") - - elif subcmd == "graphml": - from graphify.export import to_graphml as _to_graphml - _to_graphml(G, communities, str(out_dir / "graph.graphml")) - print(f"graph.graphml written - open in Gephi, yEd, or any GraphML tool") - - elif subcmd == "neo4j": - if push_uri: - from graphify.export import push_to_neo4j as _push - if push_password is None: - print("error: --password required for --push", file=sys.stderr) - sys.exit(1) - result = _push(G, uri=push_uri, user=push_user, - communities=communities, **{"password": push_password}) - print(f"Pushed to Neo4j: {result['nodes']} nodes, {result['edges']} edges") - else: - from graphify.export import to_cypher as _to_cypher - _to_cypher(G, str(out_dir / "cypher.txt")) - print(f"cypher.txt written - import with: cypher-shell < {out_dir}/cypher.txt") - - elif subcmd == "falkordb": - if push_uri: - from graphify.export import push_to_falkordb as _push - result = _push(G, uri=push_uri, user=push_user, - communities=communities, **{"password": push_password}) - print(f"Pushed to FalkorDB: {result['nodes']} nodes, {result['edges']} edges") - else: - from graphify.export import to_cypher as _to_cypher - _to_cypher(G, str(out_dir / "cypher.txt")) - print(f"cypher.txt written ({out_dir}/cypher.txt) - statements are OpenCypher. " - f"FalkorDB's GRAPH.QUERY runs one statement at a time (no bulk script " - f"import), so load a graph with: graphify export falkordb --push " - f"falkordb://localhost:6379") - - elif cmd == "benchmark": - from graphify.benchmark import run_benchmark, print_benchmark - - graph_path = sys.argv[2] if len(sys.argv) > 2 else _default_graph_path() - _enforce_graph_size_cap_or_exit(Path(graph_path)) - # Try to load corpus_words from detect output - corpus_words = None - detect_path = Path(".graphify_detect.json") - if detect_path.exists(): - try: - detect_data = json.loads(detect_path.read_text(encoding="utf-8")) - corpus_words = detect_data.get("total_words") - except Exception: - pass - result = run_benchmark(graph_path, corpus_words=corpus_words) - print_benchmark(result) - - elif cmd == "global": - subcmd = sys.argv[2] if len(sys.argv) > 2 else "" - from graphify.global_graph import ( - global_add as _global_add, - global_remove as _global_remove, - global_list as _global_list, - global_path as _global_path, - ) - if subcmd == "add": - # graphify global add [--as ] - args = sys.argv[3:] - source = None - tag = None - i = 0 - while i < len(args): - if args[i] == "--as" and i + 1 < len(args): - tag = args[i + 1]; i += 2 - elif not source: - source = Path(args[i]); i += 1 - else: - i += 1 - if not source: - print("Usage: graphify global add [--as ]", file=sys.stderr) - sys.exit(1) - tag = tag or source.parent.parent.name - try: - result = _global_add(source, tag) - if result["skipped"]: - print(f"'{tag}' unchanged since last add - global graph not modified.") - else: - print(f"Added '{tag}' to global graph: +{result['nodes_added']} nodes, " - f"-{result['nodes_removed']} pruned. Global: {_global_path()}") - except Exception as exc: - print(f"error: {exc}", file=sys.stderr); sys.exit(1) - elif subcmd == "remove": - tag = sys.argv[3] if len(sys.argv) > 3 else "" - if not tag: - print("Usage: graphify global remove ", file=sys.stderr); sys.exit(1) - try: - removed = _global_remove(tag) - print(f"Removed '{tag}' from global graph ({removed} nodes pruned).") - except KeyError as exc: - print(f"error: {exc}", file=sys.stderr); sys.exit(1) - elif subcmd == "list": - repos = _global_list() - if not repos: - print("Global graph is empty. Use 'graphify global add' to add a project.") - else: - print(f"Global graph: {_global_path()}") - for tag, info in repos.items(): - print(f" {tag}: {info.get('node_count', '?')} nodes, added {info.get('added_at', '?')[:10]}") - elif subcmd == "path": - print(_global_path()) - else: - print("Usage: graphify global [add|remove|list|path]", file=sys.stderr); sys.exit(1) - - elif cmd == "extract": - # Headless full-pipeline extraction for CI / scripts (#698). - # Runs detect -> AST extraction on code -> semantic LLM extraction on - # docs/papers/images -> merge -> build -> cluster -> write outputs. - # Unlike the skill.md path (which runs through Claude Code subagents), - # this calls extract_corpus_parallel directly using the auto-detected - # local Ollama primary with ordered local fallback and MiniMax last. - if len(sys.argv) < 3: - print( - "Usage: graphify extract [--backend ollama|minimax|nim|gemini|kimi|claude|openai|deepseek] " - "[--model M] [--mode deep] [--out DIR] [--google-workspace] [--no-cluster] " - "[--max-workers N] [--token-budget N] [--max-concurrency N] " - "[--api-timeout S] [--postgres DSN] [--cargo] [--timing]", - file=sys.stderr, - ) - sys.exit(1) - - has_path = True - if sys.argv[2].startswith("-"): - has_path = False - target = Path(".").resolve() - else: - target = Path(sys.argv[2]).resolve() - if not target.exists(): - print(f"error: path not found: {target}", file=sys.stderr) - sys.exit(1) - - backend: str | None = None - model: str | None = None - extract_mode: str | None = None - out_dir: Path | None = None - cli_postgres_dsn: str | None = None - cli_cargo: bool = False - no_cluster = False - dedup_llm = False - google_workspace = False - global_merge = False - global_repo_tag: str | None = None - # Performance/tuning knobs (issue #792). None means "use library default". - cli_max_workers: int | None = None - cli_token_budget: int | None = None - cli_max_concurrency: int | None = None - cli_api_timeout: float | None = None - # Clustering tuning knobs - cli_resolution: float = 1.0 - cli_exclude_hubs: float | None = None - cli_excludes: list[str] = [] - cli_timing: bool = False - - def _parse_int(name: str, raw: str) -> int: - try: - v = int(raw) - except ValueError: - print(f"error: {name} must be a positive integer (got {raw!r})", file=sys.stderr) - sys.exit(2) - if v <= 0: - print(f"error: {name} must be > 0 (got {v})", file=sys.stderr) - sys.exit(2) - return v - - def _parse_float(name: str, raw: str) -> float: - try: - v = float(raw) - except ValueError: - print(f"error: {name} must be a positive number (got {raw!r})", file=sys.stderr) - sys.exit(2) - if v <= 0: - print(f"error: {name} must be > 0 (got {v})", file=sys.stderr) - sys.exit(2) - return v - - args = sys.argv[3:] if has_path else sys.argv[2:] - i = 0 - while i < len(args): - a = args[i] - if a == "--backend" and i + 1 < len(args): - backend = args[i + 1]; i += 2 - elif a.startswith("--backend="): - backend = a.split("=", 1)[1]; i += 1 - elif a == "--model" and i + 1 < len(args): - model = args[i + 1]; i += 2 - elif a.startswith("--model="): - model = a.split("=", 1)[1]; i += 1 - elif a == "--mode" and i + 1 < len(args): - extract_mode = args[i + 1]; i += 2 - elif a.startswith("--mode="): - extract_mode = a.split("=", 1)[1]; i += 1 - elif a == "--out" and i + 1 < len(args): - out_dir = Path(args[i + 1]); i += 2 - elif a.startswith("--out="): - out_dir = Path(a.split("=", 1)[1]); i += 1 - elif a == "--no-cluster": - no_cluster = True; i += 1 - elif a == "--dedup-llm": - dedup_llm = True; i += 1 - elif a == "--google-workspace": - google_workspace = True; i += 1 - elif a == "--global": - global_merge = True; i += 1 - elif a == "--as" and i + 1 < len(args): - global_repo_tag = args[i + 1]; i += 2 - elif a == "--max-workers" and i + 1 < len(args): - cli_max_workers = _parse_int("--max-workers", args[i + 1]); i += 2 - elif a.startswith("--max-workers="): - cli_max_workers = _parse_int("--max-workers", a.split("=", 1)[1]); i += 1 - elif a == "--token-budget" and i + 1 < len(args): - cli_token_budget = _parse_int("--token-budget", args[i + 1]); i += 2 - elif a.startswith("--token-budget="): - cli_token_budget = _parse_int("--token-budget", a.split("=", 1)[1]); i += 1 - elif a == "--max-concurrency" and i + 1 < len(args): - cli_max_concurrency = _parse_int("--max-concurrency", args[i + 1]); i += 2 - elif a.startswith("--max-concurrency="): - cli_max_concurrency = _parse_int("--max-concurrency", a.split("=", 1)[1]); i += 1 - elif a == "--api-timeout" and i + 1 < len(args): - cli_api_timeout = _parse_float("--api-timeout", args[i + 1]); i += 2 - elif a.startswith("--api-timeout="): - cli_api_timeout = _parse_float("--api-timeout", a.split("=", 1)[1]); i += 1 - elif a == "--resolution" and i + 1 < len(args): - cli_resolution = _parse_float("--resolution", args[i + 1]); i += 2 - elif a.startswith("--resolution="): - cli_resolution = _parse_float("--resolution", a.split("=", 1)[1]); i += 1 - elif a == "--exclude-hubs" and i + 1 < len(args): - cli_exclude_hubs = float(args[i + 1]); i += 2 - elif a.startswith("--exclude-hubs="): - cli_exclude_hubs = float(a.split("=", 1)[1]); i += 1 - elif a == "--exclude" and i + 1 < len(args): - cli_excludes.append(args[i + 1]); i += 2 - elif a.startswith("--exclude="): - cli_excludes.append(a.split("=", 1)[1]); i += 1 - elif a == "--postgres" and i + 1 < len(args): - cli_postgres_dsn = args[i + 1]; i += 2 - elif a.startswith("--postgres="): - cli_postgres_dsn = a.split("=", 1)[1]; i += 1 - elif a == "--cargo": - cli_cargo = True - i += 1 - elif a == "--timing": - cli_timing = True; i += 1 - else: - i += 1 - - if not has_path and cli_postgres_dsn is None: - print("error: must specify a path to scan or a --postgres DSN", file=sys.stderr) - sys.exit(1) - - _VALID_MODES = {"deep"} - if extract_mode is not None and extract_mode not in _VALID_MODES: - print( - f"error: unknown --mode '{extract_mode}'. " - f"Available: {', '.join(sorted(_VALID_MODES))}", - file=sys.stderr, - ) - sys.exit(2) - deep_mode = extract_mode == "deep" - if deep_mode: - print("[graphify extract] deep mode enabled: richer semantic extraction") - - # CLI flag wins over env var. Setting GRAPHIFY_API_TIMEOUT here so - # _call_openai_compat picks it up without needing a new kwarg path. - if cli_api_timeout is not None: - os.environ["GRAPHIFY_API_TIMEOUT"] = str(cli_api_timeout) - if cli_max_workers is not None: - os.environ["GRAPHIFY_MAX_WORKERS"] = str(cli_max_workers) - - # Resolve output dir. The user-facing contract is "/graphify-out/" - # so a fresh checkout writes graphify-out/ at the project root, matching - # the skill.md pipeline. - out_root = (out_dir.resolve() if out_dir else target) - graphify_out = out_root / _GRAPHIFY_OUT - graphify_out.mkdir(parents=True, exist_ok=True) - - stages = _StageTimer(cli_timing) - - from graphify.detect import ( - detect as _detect, - detect_incremental as _detect_incremental, - save_manifest as _save_manifest, - ) - manifest_path = graphify_out / "manifest.json" - existing_graph_path = graphify_out / "graph.json" - incremental_mode = manifest_path.exists() and existing_graph_path.exists() if has_path else False - - if not has_path: - code_files = [] - doc_files = [] - paper_files = [] - image_files = [] - deleted_files = [] - unchanged_total = 0 - files_by_type = {} - elif incremental_mode: - print(f"[graphify extract] incremental scan of {target}") - detection = _detect_incremental( - target, - manifest_path=str(manifest_path), - google_workspace=google_workspace or None, - extra_excludes=cli_excludes or None, - ) - files_by_type = detection.get("files", {}) - new_by_type = detection.get("new_files", {}) - code_files = [Path(p) for p in new_by_type.get("code", [])] - doc_files = [Path(p) for p in new_by_type.get("document", [])] - paper_files = [Path(p) for p in new_by_type.get("paper", [])] - image_files = [Path(p) for p in new_by_type.get("image", [])] - deleted_files = list(detection.get("deleted_files", [])) - unchanged_total = sum(len(v) for v in detection.get("unchanged_files", {}).values()) - else: - print(f"[graphify extract] scanning {target}") - detection = _detect(target, google_workspace=google_workspace or None, extra_excludes=cli_excludes or None) - files_by_type = detection.get("files", {}) - code_files = [Path(p) for p in files_by_type.get("code", [])] - doc_files = [Path(p) for p in files_by_type.get("document", [])] - paper_files = [Path(p) for p in files_by_type.get("paper", [])] - image_files = [Path(p) for p in files_by_type.get("image", [])] - deleted_files = [] - unchanged_total = 0 - - semantic_files = doc_files + paper_files + image_files - if incremental_mode: - print( - f"[graphify extract] {len(code_files)} code, {len(doc_files)} docs, " - f"{len(paper_files)} papers, {len(image_files)} images changed; " - f"{unchanged_total} unchanged; {len(deleted_files)} deleted" - ) - else: - print( - f"[graphify extract] found {len(code_files)} code, " - f"{len(doc_files)} docs, {len(paper_files)} papers, " - f"{len(image_files)} images" - ) - stages.mark("detect") - - # Resolve the LLM backend only now that we know whether the corpus - # needs one. A code-only corpus is pure local AST and must not require - # an API key; the key is enforced below only when there's LLM work. - from graphify.llm import ( - BACKENDS as _BACKENDS, - detect_backend as _detect_backend, - estimate_cost as _estimate_cost, - extract_corpus_parallel as _extract_corpus_parallel, - _format_backend_env_keys, - _get_backend_api_key, - ) - needs_llm = bool(semantic_files) or dedup_llm - auto_backend = backend is None and needs_llm - if backend is None and needs_llm: - backend = _detect_backend() - if backend is not None and backend not in _BACKENDS: - print( - f"error: unknown backend '{backend}'. " - f"Available: {', '.join(sorted(_BACKENDS))}", - file=sys.stderr, - ) - sys.exit(1) - if needs_llm: - if backend is None: - reasons = [] - if semantic_files: - reasons.append( - f"{len(semantic_files)} doc/paper/image file(s) need semantic extraction" - ) - if dedup_llm: - reasons.append("--dedup-llm was passed") - print( - "error: no LLM backend found (" + "; ".join(reasons) + "). " - "Graphify auto-detects local Ollama first (default model " - "qwen2.5-coder:3b, <=8B local safety class) and MiniMax as token-plan fallback. " - "Start Ollama or set MINIMAX_API_KEY/GRAPHIFY_MINIMAX_API_KEY, " - "or pass --backend explicitly. A code-only corpus needs no key.", - file=sys.stderr, - ) - sys.exit(1) - if backend == "ollama": - from graphify.llm import _validate_ollama_base_url - _oll_url = os.environ.get("OLLAMA_BASE_URL", _BACKENDS["ollama"].get("base_url", "")) - try: - _validate_ollama_base_url(_oll_url, warn=False) - except ValueError as exc: - print(f"error: {exc}", file=sys.stderr) - sys.exit(2) - if not _get_backend_api_key(backend): - allow_no_key = False - if backend == "ollama": - from urllib.parse import urlparse - ollama_url = os.environ.get( - "OLLAMA_BASE_URL", - _BACKENDS["ollama"].get("base_url", ""), - ) - try: - host = (urlparse(ollama_url).hostname or "").lower() - except Exception: - host = "" - allow_no_key = ( - host in ("localhost", "127.0.0.1", "::1") - or host.startswith("127.") - ) - elif backend == "bedrock": - allow_no_key = bool( - os.environ.get("AWS_PROFILE") - or os.environ.get("AWS_REGION") - or os.environ.get("AWS_DEFAULT_REGION") - or os.environ.get("AWS_ACCESS_KEY_ID") - ) - elif backend == "claude-cli": - import shutil as _shutil - allow_no_key = _shutil.which("claude") is not None - if not allow_no_key: - print( - "error: backend 'claude-cli' requires the `claude` CLI on $PATH " - "(install Claude Code and run `claude` once to authenticate).", - file=sys.stderr, - ) - sys.exit(1) - if not allow_no_key: - print( - f"error: backend '{backend}' requires {_format_backend_env_keys(backend)} to be set.", - file=sys.stderr, - ) - sys.exit(1) - - # AST extraction on code files. Empty code list (docs-only corpus) is - # the issue #698 case — skip cleanly instead of crashing inside extract(). - ast_result: dict = {"nodes": [], "edges": [], "input_tokens": 0, "output_tokens": 0} - if code_files: - from graphify.extract import extract as _ast_extract - # Anchor the cache at the output root, not the scanned project: - # with --out, a /graphify-out/cache/ would leak a - # graphify-out/ dir into a project that asked for external output. - ast_kwargs: dict = {"cache_root": out_root} - if cli_max_workers is not None: - ast_kwargs["max_workers"] = cli_max_workers - print(f"[graphify extract] AST extraction on {len(code_files)} code files...") - try: - ast_result = _ast_extract(code_files, **ast_kwargs) - except Exception as exc: - print(f"[graphify extract] AST extraction failed: {exc}", file=sys.stderr) - ast_result = {"nodes": [], "edges": [], "input_tokens": 0, "output_tokens": 0} - stages.mark("AST extract") - - # Semantic extraction on docs/papers/images. Check cache first. - from graphify.cache import ( - check_semantic_cache as _check_semantic_cache, - prune_semantic_cache as _prune_semantic_cache, - save_semantic_cache as _save_semantic_cache, - ) - sem_result: dict = { - "nodes": [], "edges": [], "hyperedges": [], - "input_tokens": 0, "output_tokens": 0, - } - sem_cache_hits = 0 - sem_cache_misses = 0 - if semantic_files: - sem_paths_str = [str(p) for p in semantic_files] - cached_nodes, cached_edges, cached_hyperedges, uncached_paths = ( - _check_semantic_cache(sem_paths_str, root=out_root) - ) - sem_cache_hits = len(semantic_files) - len(uncached_paths) - sem_cache_misses = len(uncached_paths) - sem_result["nodes"].extend(cached_nodes) - sem_result["edges"].extend(cached_edges) - sem_result["hyperedges"].extend(cached_hyperedges) - if sem_cache_hits: - print(f"[graphify extract] semantic cache: {sem_cache_hits} hit / {sem_cache_misses} miss") - - if uncached_paths: - print(f"[graphify extract] semantic extraction on {len(uncached_paths)} files via {backend}...") - corpus_kwargs: dict = { - "backend": backend, - "model": model, - "root": target, - "allow_minimax_fallback": auto_backend or backend == "ollama", - } - if deep_mode: - corpus_kwargs["deep_mode"] = True - if cli_token_budget is not None: - corpus_kwargs["token_budget"] = cli_token_budget - if cli_max_concurrency is not None: - corpus_kwargs["max_concurrency"] = cli_max_concurrency - - # Minimal progress callback so the CLI is no longer silent - # during long local-inference runs (issue #792 addendum). - # Also track per-chunk success so we can fail loudly when - # every chunk errors (e.g. missing backend SDK package). - _chunk_stats = {"total": 0, "succeeded": 0} - def _progress(idx: int, total: int, _result: dict) -> None: - _chunk_stats["total"] = total - _chunk_stats["succeeded"] += 1 - print( - f"[graphify extract] chunk {idx + 1}/{total} done", - flush=True, - ) - corpus_kwargs["on_chunk_done"] = _progress - - try: - fresh = _extract_corpus_parallel( - [Path(p) for p in uncached_paths], - **corpus_kwargs, - ) - except ImportError as exc: - print(f"error: {exc}", file=sys.stderr) - sys.exit(1) - except Exception as exc: - print( - f"[graphify extract] semantic extraction failed: {exc}", - file=sys.stderr, - ) - fresh = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 0, "output_tokens": 0} - - if fresh.get("deferred_semantic"): - queue = graphify_out / "semantic-rebuild-queue.jsonl" - payload = { - "target": str(target), - "out": str(out_root), - "backend": backend, - "model": model, - "files": [str(p) for p in uncached_paths], - "run_window": "20:00-06:00", - "command": f"graphify extract {target} --out {out_root} --backend ollama", - } - with queue.open("a", encoding="utf-8") as fh: - fh.write(json.dumps(payload, sort_keys=True) + "\n") - print( - f"[graphify extract] semantic rebuild deferred; queued night job hint in {queue}", - file=sys.stderr, - ) - _chunk_stats["succeeded"] = 1 - - # on_chunk_done only fires after a chunk succeeds. If fresh - # semantic extraction was requested and no chunks completed, - # fail instead of writing an AST-only graph with exit 0. - if uncached_paths and _chunk_stats["succeeded"] == 0: - print( - f"[graphify extract] error: all semantic chunks failed " - f"for backend '{backend}' ({len(uncached_paths)} uncached files) - " - f"see per-chunk errors above. If you see 'requires the X package', " - f"run `pip install X` and retry.", - file=sys.stderr, - ) - sys.exit(1) - try: - _save_semantic_cache( - fresh.get("nodes", []), - fresh.get("edges", []), - fresh.get("hyperedges", []), - root=out_root, - ) - except Exception as exc: - print(f"[graphify extract] warning: could not write semantic cache: {exc}", file=sys.stderr) - sem_result["nodes"].extend(fresh.get("nodes", [])) - sem_result["edges"].extend(fresh.get("edges", [])) - sem_result["hyperedges"].extend(fresh.get("hyperedges", [])) - sem_result["input_tokens"] += fresh.get("input_tokens", 0) - sem_result["output_tokens"] += fresh.get("output_tokens", 0) - - # Prune orphaned semantic cache entries. The semantic cache is - # content-hash-keyed and unversioned, so it is never swept by the AST - # version-cleanup: every content change or file deletion leaves a - # permanent orphan that accumulates unbounded (#1527). Sweep it against - # the FULL live document set (``files_by_type`` — present in both the - # incremental and full branches), NOT the incremental ``semantic_files`` - # changed-subset, which would delete every unchanged doc's valid entry. - # Best-effort: a prune failure must never break extraction. - try: - from graphify.cache import file_hash as _file_hash - _live_hashes: set[str] = set() - for _kind in ("document", "paper", "image"): - for _fp in files_by_type.get(_kind, []): - _abs = Path(_fp) - if not _abs.is_absolute(): - _abs = Path(out_root) / _abs - if not _abs.is_file(): - continue # deleted/missing — leave out so its entry is pruned - try: - _live_hashes.add(_file_hash(_abs, out_root)) - except OSError: - pass - _prune_semantic_cache(out_root, _live_hashes) - except Exception as exc: - print(f"[graphify extract] warning: could not prune semantic cache: {exc}", file=sys.stderr) - stages.mark("semantic extract") - - pg_result: dict = {"nodes": [], "edges": []} - if cli_postgres_dsn is not None: - from graphify.pg_introspect import introspect_postgres - print(f"[graphify extract] introspecting PostgreSQL schema...") - try: - pg_result = introspect_postgres(cli_postgres_dsn) - except (ConnectionError, ImportError) as exc: - print(f"error: {exc}", file=sys.stderr) - sys.exit(1) - print(f"[graphify extract] PostgreSQL: {len(pg_result['nodes'])} nodes, " - f"{len(pg_result['edges'])} edges") - - cargo_result: dict = {"nodes": [], "edges": []} - if cli_cargo: - from graphify.cargo_introspect import introspect_cargo - print("[graphify extract] introspecting Cargo workspace...") - try: - cargo_result = introspect_cargo(target) - except (ConnectionError, ImportError, OSError) as exc: - print(f"error: {exc}", file=sys.stderr) - sys.exit(1) - print(f"[graphify extract] Cargo: {len(cargo_result['nodes'])} nodes, " - f"{len(cargo_result['edges'])} edges") - - # Merge AST + semantic + pg_result + cargo_result. Order matters for deduplication: passing AST - # first means semantic node attributes win on collision (richer labels - # for symbols also referenced in docs). Hyperedges only come from the - # semantic side. - merged: dict = { - "nodes": list(ast_result.get("nodes", [])) + list(sem_result.get("nodes", [])) + list(pg_result.get("nodes", [])) + list(cargo_result.get("nodes", [])), - "edges": list(ast_result.get("edges", [])) + list(sem_result.get("edges", [])) + list(pg_result.get("edges", [])) + list(cargo_result.get("edges", [])), - "hyperedges": list(sem_result.get("hyperedges", [])), - "input_tokens": ast_result.get("input_tokens", 0) + sem_result.get("input_tokens", 0), - "output_tokens": ast_result.get("output_tokens", 0) + sem_result.get("output_tokens", 0), - } - - graph_json_path = graphify_out / "graph.json" - analysis_path = graphify_out / ".graphify_analysis.json" - - # Build a manifest-safe files dict: only stamp semantic_hash for files - # that actually produced output (cache hit or fresh extraction). Files - # whose chunk failed have no source_file entry in sem_result — leaving - # their semantic_hash empty so detect_incremental re-queues them (#933). - _sem_extracted: set[str] = { - n.get("source_file", "") for n in sem_result.get("nodes", []) - } | { - e.get("source_file", "") for e in sem_result.get("edges", []) - } - _sem_extracted.discard("") - _sem_types = {"document", "paper", "image"} - _manifest_files = { - ftype: [f for f in flist if ftype not in _sem_types or f in _sem_extracted] - for ftype, flist in files_by_type.items() - } - - if no_cluster: - # --no-cluster: dump the raw merged extraction as graph.json. - # No NetworkX, no community detection, no analysis sidecar. - # Dedupe nodes (by id) and parallel edges so the raw output matches the - # clustered path (whose DiGraph collapses both) and stays deterministic - # across modes (#1317; node dedup also collapses shared Swift module - # anchors emitted per importing file, #1327). - from graphify.build import dedupe_edges as _dedupe_edges, dedupe_nodes as _dedupe_nodes - from graphify.export import backup_if_protected as _backup - if ( - incremental_mode - and not code_files - and not semantic_files - and not deleted_files - and not pg_result.get("nodes") - and not pg_result.get("edges") - and not cargo_result.get("nodes") - and not cargo_result.get("edges") - ): - print( - "[graphify extract] no incremental changes detected " - "(--no-cluster); outputs left untouched." - ) - try: - _save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both", root=target) - except Exception as exc: - print(f"[graphify extract] warning: could not write manifest: {exc}", file=sys.stderr) - stages.total() - sys.exit(0) - - merged["nodes"] = _dedupe_nodes(merged["nodes"]) - merged["edges"] = _dedupe_edges(merged["edges"]) - # Backfill source_file from endpoint nodes — this raw path bypasses - # build_from_json's backfill, and semantic edges sometimes omit it (#1279). - _node_sf = {n.get("id"): n.get("source_file") for n in merged["nodes"]} - for _e in merged["edges"]: - if not _e.get("source_file"): - _e["source_file"] = ( - _node_sf.get(_e.get("source")) or _node_sf.get(_e.get("target")) or "" - ) - _backup(graphify_out) - graph_json_path.write_text( - json.dumps(merged, indent=2), encoding="utf-8" - ) - stages.mark("write") - cost = _estimate_cost( - backend, merged["input_tokens"], merged["output_tokens"] - ) - print( - f"[graphify extract] wrote {graph_json_path} — " - f"{len(merged['nodes'])} nodes, {len(merged['edges'])} edges " - f"(no clustering)" - ) - if merged["input_tokens"] or merged["output_tokens"]: - print( - f"[graphify extract] tokens: " - f"{merged['input_tokens']:,} in / " - f"{merged['output_tokens']:,} out, " - f"est. cost: ${cost:.4f}" - ) - try: - _save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both", root=target) - except Exception as exc: - print(f"[graphify extract] warning: could not write manifest: {exc}", file=sys.stderr) - if global_merge: - from graphify.global_graph import global_add as _global_add - _tag = global_repo_tag or target.name - try: - result = _global_add(graphify_out / "graph.json", _tag) - if result["skipped"]: - print(f"[graphify global] '{_tag}' unchanged since last add - skipped.") - else: - print(f"[graphify global] '{_tag}' merged into global graph " - f"(+{result['nodes_added']} nodes, -{result['nodes_removed']} pruned).") - except Exception as exc: - print(f"[graphify global] warning: failed to merge into global graph: {exc}", file=sys.stderr) - stages.total() - sys.exit(0) - - # Build graph + cluster + score + write. - from graphify.build import ( - build as _build, - build_from_json as _build_from_json, - build_merge as _build_merge, - ) - from graphify.cluster import cluster as _cluster, score_all as _score_all - from graphify.export import to_json as _to_json - from graphify.analyze import god_nodes as _god_nodes, surprising_connections as _surprising - dedup_backend = backend if dedup_llm else None - if incremental_mode: - G = _build_merge( - [merged], - graph_path=existing_graph_path, - prune_sources=deleted_files or None, - dedup=True, - dedup_llm_backend=dedup_backend, - root=target, - ) - else: - G = _build([merged], dedup=True, dedup_llm_backend=dedup_backend, root=target) - stages.mark("build") - if G.number_of_nodes() == 0: - print( - "[graphify extract] graph is empty — extraction produced no nodes. " - "Possible causes: all files skipped, binary-only corpus, or LLM " - "returned no edges.", - file=sys.stderr, - ) - sys.exit(1) - - communities = _cluster(G, resolution=cli_resolution, exclude_hubs_percentile=cli_exclude_hubs) - stages.mark("cluster") - cohesion = _score_all(G, communities) - try: - gods = _god_nodes(G) - except Exception: - gods = [] - try: - surprises = _surprising(G, communities) - except Exception: - surprises = [] - stages.mark("analyze") - - from graphify.export import backup_if_protected as _backup - _backup(graphify_out) - _to_json(G, communities, str(graph_json_path), force=True) - stages.mark("export") - if merged.get("output_tokens", 0) > 0: - (graphify_out / ".graphify_semantic_marker").write_text( - json.dumps({"output_tokens": merged["output_tokens"]}), encoding="utf-8" - ) - if global_merge: - from graphify.global_graph import global_add as _global_add - _tag = global_repo_tag or target.name - try: - result = _global_add(graphify_out / "graph.json", _tag) - if result["skipped"]: - print(f"[graphify global] '{_tag}' unchanged since last add - skipped.") - else: - print(f"[graphify global] '{_tag}' merged into global graph " - f"(+{result['nodes_added']} nodes, -{result['nodes_removed']} pruned).") - except Exception as exc: - print(f"[graphify global] warning: failed to merge into global graph: {exc}", file=sys.stderr) - analysis = { - "communities": {str(k): v for k, v in communities.items()}, - "cohesion": {str(k): v for k, v in cohesion.items()}, - "gods": gods, - "surprises": surprises, - "tokens": { - "input": merged["input_tokens"], - "output": merged["output_tokens"], - }, - } - analysis_path.write_text(json.dumps(analysis, indent=2), encoding="utf-8") - try: - _save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both", root=target) - except Exception as exc: - print(f"[graphify extract] warning: could not write manifest: {exc}", file=sys.stderr) - - cost = _estimate_cost(backend, merged["input_tokens"], merged["output_tokens"]) - print( - f"[graphify extract] wrote {graph_json_path}: " - f"{G.number_of_nodes()} nodes, {G.number_of_edges()} edges, " - f"{len(communities)} communities" - ) - print(f"[graphify extract] wrote {analysis_path}") - if incremental_mode: - print( - f"[graphify extract] incremental summary: " - f"{sem_cache_hits + unchanged_total} files cached/unchanged, " - f"{len(code_files) + sem_cache_misses} re-extracted, " - f"{len(deleted_files)} deleted" - ) - elif sem_cache_hits: - print(f"[graphify extract] semantic cache: {sem_cache_hits} cached, {sem_cache_misses} re-extracted") - if merged["input_tokens"] or merged["output_tokens"]: - print( - f"[graphify extract] tokens: " - f"{merged['input_tokens']:,} in / " - f"{merged['output_tokens']:,} out, " - f"est. cost (~{backend}): ${cost:.4f}" - ) - # extract intentionally stops at graph.json + analysis; the report and - # community labels are produced by `cluster-only` (or an agent's Step 5). - # Point standalone users at it so communities get named (#1097). - print( - "[graphify extract] next: run " - f"`graphify cluster-only {graphify_out.parent}` " - "to generate GRAPH_REPORT.md and name communities" - ) - stages.total() - - elif cmd == "cache-check": - # graphify cache-check [--root ] - # Reads file paths (one per line) from , checks semantic cache. - # Writes: - # graphify-out/.graphify_cached.json — already-cached nodes/edges/hyperedges - # graphify-out/.graphify_uncached.txt — paths that need extraction - # Stdout: "Cache: N hit, M miss" - from graphify.cache import check_semantic_cache - if len(sys.argv) < 3: - print("Usage: graphify cache-check [--root ]", file=sys.stderr) - sys.exit(1) - files_from = Path(sys.argv[2]) - root = Path(".") - i = 3 - while i < len(sys.argv): - if sys.argv[i] == "--root" and i + 1 < len(sys.argv): - root = Path(sys.argv[i + 1]) - i += 2 - else: - i += 1 - files = [f for f in files_from.read_text(encoding="utf-8").splitlines() if f.strip()] - cached_nodes, cached_edges, cached_hyperedges, uncached = check_semantic_cache(files, root) - out = root / _GRAPHIFY_OUT - out.mkdir(parents=True, exist_ok=True) - if cached_nodes or cached_edges or cached_hyperedges: - (out / ".graphify_cached.json").write_text( - json.dumps({"nodes": cached_nodes, "edges": cached_edges, "hyperedges": cached_hyperedges}, - ensure_ascii=False), - encoding="utf-8", - ) - (out / ".graphify_uncached.txt").write_text("\n".join(uncached), encoding="utf-8") - print(f"Cache: {len(files) - len(uncached)} hit, {len(uncached)} miss") - - elif cmd == "merge-chunks": - # graphify merge-chunks --out - # Concatenates .graphify_chunk_*.json files written by semantic subagents. - # Deduplicates nodes by id (first writer wins). Sums token counts. - import glob as _glob - if len(sys.argv) < 3: - print("Usage: graphify merge-chunks --out ", file=sys.stderr) - sys.exit(1) - out_path: Path | None = None - chunk_args: list[str] = [] - i = 2 - while i < len(sys.argv): - if sys.argv[i] == "--out" and i + 1 < len(sys.argv): - out_path = Path(sys.argv[i + 1]) - i += 2 - else: - chunk_args.append(sys.argv[i]) - i += 1 - if not out_path: - print("error: --out required", file=sys.stderr) - sys.exit(1) - chunk_files: list[str] = [] - for arg in chunk_args: - expanded = _glob.glob(arg) - chunk_files.extend(sorted(expanded) if expanded else [arg]) - merged: dict = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 0, "output_tokens": 0} - seen_ids: set[str] = set() - for cf in chunk_files: - try: - chunk = json.loads(Path(cf).read_text(encoding="utf-8")) - except (json.JSONDecodeError, OSError) as exc: - print(f"[graphify merge-chunks] warning: skipping {cf}: {exc}", file=sys.stderr) - continue - for n in chunk.get("nodes", []): - if n.get("id") not in seen_ids: - seen_ids.add(n["id"]) - merged["nodes"].append(n) - merged["edges"].extend(chunk.get("edges", [])) - merged["hyperedges"].extend(chunk.get("hyperedges", [])) - merged["input_tokens"] += chunk.get("input_tokens", 0) - merged["output_tokens"] += chunk.get("output_tokens", 0) - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(json.dumps(merged, ensure_ascii=False), encoding="utf-8") - print( - f"Merged {len(chunk_files)} chunks: {len(merged['nodes'])} nodes, {len(merged['edges'])} edges, " - f"{merged['input_tokens']:,} in / {merged['output_tokens']:,} out tokens" - ) - - elif cmd == "merge-semantic": - # graphify merge-semantic --cached --new --out - # Merges cached semantic results with freshly-extracted chunk results. - # Deduplicates nodes by id (cached entries take priority over new ones). - if len(sys.argv) < 3: - print("Usage: graphify merge-semantic --cached --new --out ", file=sys.stderr) - sys.exit(1) - cached_path: Path | None = None - new_path: Path | None = None - out_path2: Path | None = None - i = 2 - while i < len(sys.argv): - if sys.argv[i] == "--cached" and i + 1 < len(sys.argv): - cached_path = Path(sys.argv[i + 1]); i += 2 - elif sys.argv[i] == "--new" and i + 1 < len(sys.argv): - new_path = Path(sys.argv[i + 1]); i += 2 - elif sys.argv[i] == "--out" and i + 1 < len(sys.argv): - out_path2 = Path(sys.argv[i + 1]); i += 2 - else: - i += 1 - if not out_path2: - print("error: --out required", file=sys.stderr) - sys.exit(1) - empty: dict = {"nodes": [], "edges": [], "hyperedges": []} - cached_data = json.loads(cached_path.read_text(encoding="utf-8")) if cached_path and cached_path.exists() else empty - new_data = json.loads(new_path.read_text(encoding="utf-8")) if new_path and new_path.exists() else empty - seen_ids2: set[str] = set() - all_nodes: list[dict] = [] - for n in cached_data.get("nodes", []) + new_data.get("nodes", []): - if n.get("id") not in seen_ids2: - seen_ids2.add(n["id"]) - all_nodes.append(n) - merged2 = { - "nodes": all_nodes, - "edges": cached_data.get("edges", []) + new_data.get("edges", []), - "hyperedges": cached_data.get("hyperedges", []) + new_data.get("hyperedges", []), - } - out_path2.parent.mkdir(parents=True, exist_ok=True) - out_path2.write_text(json.dumps(merged2, ensure_ascii=False), encoding="utf-8") - print(f"Merged: {len(merged2['nodes'])} nodes, {len(merged2['edges'])} edges") - - elif Path(cmd).exists() or cmd in (".", "..") or cmd.startswith(("./", "../", "/", "~")): - # User ran `graphify ` directly — treat as `graphify extract `. - # Common when following the PowerShell note in README (`graphify .`) or - # copy-pasting skill invocations without the leading slash. - sys.argv.insert(2, sys.argv[1]) - sys.argv[1] = "extract" - main() - else: - print(f"error: unknown command '{cmd}'", file=sys.stderr) - print("Run 'graphify --help' for usage.", file=sys.stderr) - sys.exit(1) + if dispatch_install_cli(cmd): + return + dispatch_command(cmd) if __name__ == "__main__": diff --git a/graphify/affected.py b/graphify/affected.py index 69b679f9f..deacc39c8 100644 --- a/graphify/affected.py +++ b/graphify/affected.py @@ -11,6 +11,7 @@ DEFAULT_AFFECTED_RELATIONS = ( "calls", + "indirect_call", "references", "imports", "imports_from", @@ -148,6 +149,27 @@ def affected_nodes( queue: deque[tuple[str, int]] = deque([(seed, 0)]) hits: list[AffectedHit] = [] + # #1669: seed the reverse walk with the root's own member nodes (one outward + # `method`/`contains` hop). A caller can bind to a class's method node rather + # than the class node itself (e.g. `Service.call` resolves to the `def + # self.call` node, #1634), so those callers are unreachable from the class + # otherwise. The member nodes are seeds only (not reported as hits), and + # `method`/`contains` stay out of the general relation-filtered walk, so this + # adds no forward noise anywhere else. + if hasattr(graph, "out_edges"): + member_edges = graph.out_edges(seed, data=True) + else: + member_edges = ( + (s, t, d) for s, t, d in graph.edges(data=True) if s == seed + ) + for _s, member, data in member_edges: + if str(data.get("relation", "")) not in ("method", "contains"): + continue + member = str(member) + if member not in seen: + seen.add(member) + queue.append((member, 0)) + while queue: current, current_depth = queue.popleft() if current_depth >= depth: @@ -209,7 +231,13 @@ def load_graph(path: Path) -> nx.Graph: import json from networkx.readwrite import json_graph - raw = json.loads(path.read_text(encoding="utf-8")) + try: + raw = json.loads(path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError) as exc: + raise RuntimeError( + f"Cannot read graph file {path}: {exc}. " + "Re-run 'graphify extract' to regenerate it." + ) from exc # Force directed so stored caller→callee direction survives the round-trip; # mirrors serve.py and __main__.py (#1174). raw = {**raw, "directed": True} diff --git a/graphify/analyze.py b/graphify/analyze.py index 431ee41c9..7f3eb72ff 100644 --- a/graphify/analyze.py +++ b/graphify/analyze.py @@ -23,12 +23,12 @@ # Language families — extensions sharing a runtime can legitimately call each other _LANG_FAMILY: dict[str, str] = { **{e: "python" for e in (".py", ".pyw")}, - **{e: "js" for e in (".js", ".jsx", ".mjs", ".ejs", ".ts", ".tsx", ".vue", ".svelte")}, + **{e: "js" for e in (".js", ".jsx", ".mjs", ".ejs", ".ts", ".tsx", ".mts", ".cts", ".vue", ".svelte")}, **{e: "go" for e in (".go",)}, **{e: "rust" for e in (".rs",)}, **{e: "jvm" for e in (".java", ".kt", ".kts", ".scala")}, **{e: "c" for e in (".c", ".h", ".cpp", ".cc", ".cxx", ".hpp")}, - **{e: "ruby" for e in (".rb",)}, + **{e: "ruby" for e in (".rb", ".rake")}, **{e: "swift" for e in (".swift",)}, **{e: "dotnet" for e in (".cs",)}, **{e: "php" for e in (".php",)}, @@ -504,7 +504,10 @@ def suggest_questions( # 4. Isolated or weakly-connected nodes → exploration questions isolated = [ n for n in G.nodes() - if G.degree(n) <= 1 and not _is_file_node(G, n) and not _is_concept_node(G, n) + if G.degree(n) <= 1 + and not _is_file_node(G, n) + and not _is_concept_node(G, n) + and G.nodes[n].get("file_type") != "rationale" ] if isolated: labels = [G.nodes[n].get("label", n) for n in isolated[:3]] @@ -663,6 +666,11 @@ def _endpoint_source_file(node_id: str) -> str: if rel not in ("imports_from", "re_exports"): continue + # Deferred `import(...)` edges are real dependencies but do not form a + # hard file-level cycle, so they are excluded from cycle detection (#1241). + if data.get("deferred"): + continue + src_file_attr = data.get("source_file", "") if not isinstance(src_file_attr, str) or not src_file_attr: continue diff --git a/graphify/build.py b/graphify/build.py index fcfa74079..0f95b84e0 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -33,6 +33,26 @@ from .validate import validate_extraction +# Language interop families, keyed by extension, for the cross-language phantom-edge +# guard in the edge loop below. Families group by REAL interop (JS/TS share a module +# graph; C/C++/ObjC share a compilation unit via headers; JVM langs share bytecode), +# so a legitimate TS->JS import or C impl->header call survives, while a Python +# `import time` binding to a `time.ts` (#1749) or a cross-language INFERRED `calls` +# edge (#1547/#1556) is dropped. Kept local to build.py (not imported from extract.py, +# which imports build.py — a cycle) and deliberately mirrors extract._LANG_FAMILY_BY_EXT. +_EDGE_LANG_FAMILY: dict[str, str] = { + ".py": "py", ".pyi": "py", + ".js": "js", ".mjs": "js", ".cjs": "js", ".jsx": "js", + ".ts": "js", ".tsx": "js", ".mts": "js", ".cts": "js", + ".go": "go", ".rs": "rs", + ".java": "jvm", ".kt": "jvm", ".scala": "jvm", ".groovy": "jvm", + ".c": "c", ".h": "c", ".cc": "c", ".cpp": "c", ".hpp": "c", + ".cxx": "c", ".hh": "c", ".hxx": "c", + ".cu": "c", ".cuh": "c", ".metal": "c", ".m": "c", ".mm": "c", + ".rb": "rb", ".rake": "rb", ".php": "php", ".cs": "cs", ".swift": "swift", ".lua": "lua", +} + + # Synonym mapper for known invalid file_type values that LLM subagents commonly # emit. Keeps semantic intent close (markdown→document, tool→code) and falls # back to "concept" for any other invalid value (see #840). @@ -53,6 +73,56 @@ } +# Hyperedge member lists are canonically keyed `nodes` (see graphify/llm.py +# extraction spec), but LLM/subagent drift and externally-supplied graph.json +# sometimes emit `members` or `node_ids`. _normalize_hyperedge_members folds +# those aliases into `nodes` at ingest so every downstream consumer reads one +# canonical key — mirroring the `from`/`to` edge-endpoint tolerance below. +_HE_MEMBER_ALIASES = ("members", "node_ids") + + +def _normalize_hyperedge_members(he: object) -> None: + """Canonicalize a hyperedge's member list onto the `nodes` key, in place. + + If `nodes` is already a list it wins (canonical), and only stray alias keys + are dropped. Otherwise the first alias (`members`, then `node_ids`) that is a + list is moved to `nodes`, deduped preserving order, with a single stderr + WARNING naming the hyperedge id and alias used. Leftover alias keys are + always removed so downstream code never re-reads them. + """ + if not isinstance(he, dict): + return + if not isinstance(he.get("nodes"), list): + for alias in _HE_MEMBER_ALIASES: + val = he.get(alias) + if isinstance(val, list): + seen: set = set() + deduped: list = [] + for ref in val: + try: + is_dupe = ref in seen + except TypeError: + is_dupe = False # unhashable ref: keep it, validator flags it + if is_dupe: + continue + try: + seen.add(ref) + except TypeError: + pass + deduped.append(ref) + he["nodes"] = deduped + print( + f"[graphify] WARNING: hyperedge " + f"'{he.get('id', '?')}' uses field '{alias}' instead of " + f"'nodes'; normalizing.", + file=sys.stderr, + ) + break + # Drop any leftover alias keys regardless of which branch ran above. + for alias in _HE_MEMBER_ALIASES: + he.pop(alias, None) + + def _norm_source_file(p: str | None, root: str | None = None) -> str | None: """Normalize path separators and relativize absolute paths. @@ -69,10 +139,43 @@ def _norm_source_file(p: str | None, root: str | None = None) -> str | None: try: p = Path(p).relative_to(root).as_posix() except ValueError: - pass + # Lexical relative_to failed. Retry with both sides fully resolved: + # a symlinked scan root (macOS /var -> /private/var, or a symlinked + # home/worktree) makes the raw prefixes differ even though they point + # at the same dir, which otherwise silently defeats prune/replace + # matching. Only the slow path resolves, so the common lexical match + # stays filesystem-free. + try: + p = Path(p).resolve().relative_to(Path(root).resolve()).as_posix() + except (ValueError, OSError): + pass return p +def _infer_merge_root(graph_path: Path) -> str | None: + """Best-effort scan root for relativizing paths in build_merge when the caller + passes no ``root`` (#1571). + + Prefers the committed ``graphify-out/.graphify_root`` marker — the authoritative + scan root graphify records at build/watch time (#686/#1423) — then falls back to + the directory that contains the output dir (``graph.json``'s grandparent, i.e. + ``/graphify-out/graph.json`` -> ````). Returns None if neither + resolves, in which case normalization is a no-op (prior behavior). + """ + try: + marker = graph_path.parent / ".graphify_root" + if marker.exists(): + recorded = marker.read_text(encoding="utf-8").strip() + if recorded: + return str(Path(recorded).resolve()) + except OSError: + pass + try: + return str(graph_path.parent.parent.resolve()) + except Exception: + return None + + def edge_data(G: nx.Graph, u: str, v: str) -> dict: """Return one edge attribute dict for (u, v), tolerating MultiGraph. @@ -262,6 +365,11 @@ def _semantic_id_remap(nodes: list, root: str | None) -> dict: rel = Path(sf_norm) if rel.is_absolute(): continue # can't relativize (no/failed root) — leave id untouched + if not rel.name: + # source_file equals the scan root, so _norm_source_file relativized it + # to Path('.') — a project-level node with no per-file identity to remap. + # Leave its id untouched (and avoid _file_stem's empty-name crash, #1618). + continue new_stem = make_id(_file_stem(rel)) if not new_stem: continue @@ -309,6 +417,8 @@ def graph_has_legacy_ids(nodes: list, root: str | Path | None = None, sample: in rel = Path(_norm_source_file(str(sf), _r) or str(sf)) if rel.is_absolute(): continue + if not rel.name: + continue # source_file == scan root -> Path('.'), no file stem (#1618) new_stem = make_id(_file_stem(rel)) if not new_stem: continue @@ -325,6 +435,38 @@ def graph_has_legacy_ids(nodes: list, root: str | Path | None = None, sample: in return False +def _doc_twin_remap(nodes: list) -> dict[str, str]: + """Map a markdown quick-scan's bare doc node ```` to the semantic + ``_doc`` node for the SAME file (#1799). + + The markdown quick-scan (``extract_markdown``) mints a file node with the + bare id ``_make_id(path)`` while the semantic pass mints ``_doc`` for + the same document. A ``graphify update`` after a semantic build leaves both, + splitting the file's edges across two disconnected nodes. Canonicalize to the + semantic ``_doc`` node (it carries the richer references/hyperedges). Gated to + ``file_type == "document"`` on BOTH twins with an identical ``source_file``, + so an unrelated code symbol ``foo`` and ``foo_doc`` never merge. + """ + by_id: dict[str, dict] = {} + for n in nodes: + if isinstance(n, dict) and n.get("id"): + by_id[str(n["id"])] = n + remap: dict[str, str] = {} + for nid, node in by_id.items(): + if not nid.endswith("_doc"): + continue + bare = by_id.get(nid[:-4]) + if bare is None: + continue + sf = node.get("source_file") + if not sf or bare.get("source_file") != sf: + continue + if node.get("file_type") != "document" or bare.get("file_type") != "document": + continue + remap[nid[:-4]] = nid + return remap + + def build_from_json(extraction: dict, *, directed: bool = False, root: str | Path | None = None) -> nx.Graph: """Build a NetworkX graph from an extraction dict. @@ -367,6 +509,14 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat if ft and ft not in {"code", "document", "paper", "image", "rationale", "concept"}: node["file_type"] = _FILE_TYPE_SYNONYMS.get(ft, "concept") + # Canonicalize hyperedge member lists (#1561): producers sometimes key the + # member list `members`/`node_ids` instead of `nodes`. Fold aliases onto + # `nodes` here — BEFORE validation and the semantic-rekey loop below — so + # every downstream consumer (rekey, source_file relativize, to_json) reads + # one canonical key, the same way edge endpoints alias from/to at build. + for he in extraction.get("hyperedges", []) or []: + _normalize_hyperedge_members(he) + errors = validate_extraction(extraction) # Dangling edges (stdlib/external imports) are expected - only warn about real schema errors. real_errors = [e for e in errors if "does not match any node id" not in e] @@ -398,6 +548,33 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat if isinstance(he, dict) and isinstance(he.get("nodes"), list): he["nodes"] = [_rekey.get(n, n) for n in he["nodes"]] + # Merge markdown quick-scan bare doc nodes into their semantic `_doc` twin + # for the same file, so a document is one node regardless of which pipeline + # touched it last (#1799). + _doc_remap = _doc_twin_remap(extraction.get("nodes", [])) + if _doc_remap: + extraction["nodes"] = [ + n for n in extraction.get("nodes", []) + if not (isinstance(n, dict) and n.get("id") in _doc_remap) + ] + _new_edges = [] + for edge in extraction.get("edges", []): + if isinstance(edge, dict): + s0, t0 = edge.get("source"), edge.get("target") + if s0 in _doc_remap: + edge["source"] = _doc_remap[s0] + if t0 in _doc_remap: + edge["target"] = _doc_remap[t0] + # Drop only self-loops the remap itself collapsed (a bare->_doc + # link becoming doc->doc); leave any pre-existing self-loop alone. + if edge.get("source") == edge.get("target") and (s0 in _doc_remap or t0 in _doc_remap): + continue + _new_edges.append(edge) + extraction["edges"] = _new_edges + for he in extraction.get("hyperedges", []) or []: + if isinstance(he, dict) and isinstance(he.get("nodes"), list): + he["nodes"] = [_doc_remap.get(n, n) for n in he["nodes"]] + G: nx.Graph = nx.DiGraph() if directed else nx.Graph() for node in extraction.get("nodes", []): # Skip dict nodes with a missing or non-hashable id (e.g. a list emitted @@ -439,7 +616,11 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat # ghost would pick an arbitrary winner via set-iteration order (#1257). Track # those keys so Pass 2 skips them — same conservatism as # _rewire_unique_stub_nodes, which only merges when exactly one real def exists. - for nid in node_set: + # Iterate in a deterministic (sorted) order, not set-iteration order, so the + # canonical winner and the ambiguity decisions below don't flip run-to-run + # with CPython's per-process string-hash seed (#1753) — the same reason the + # edge-iteration loop further down sorts on purpose. + for nid in sorted(node_set): attrs = G.nodes[nid] label = str(attrs.get("label", "")).strip() sf = str(attrs.get("source_file", "")) @@ -455,11 +636,27 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat _loc_collisions.add(key) # AST-origin nodes always overwrite a prior non-AST entry. _loc_nodes[key] = nid - elif key not in _loc_nodes: - _loc_nodes[key] = nid + else: + existing = _loc_nodes.get(key) + if existing is None: + _loc_nodes[key] = nid + elif ( + G.nodes[existing].get("_origin") != "ast" + and str(G.nodes[existing].get("source_file", "")) != sf + ): + # Two NON-AST nodes sharing (basename, label) but coming from + # DIFFERENT files are distinct concepts (e.g. a same-named + # concept in dir_a/update.md and dir_b/update.md), not an AST + # ghost/canonical twin. Merging them would drop a real node + # and pick the survivor arbitrarily via iteration order + # (#1753). Mark the key ambiguous so Pass 2 leaves both, the + # same conservatism the AST/AST case uses (#1257). A genuine + # same-file duplicate (identical source_file) is not flagged + # and still collapses. + _loc_collisions.add(key) # Pass 2: find ghosts — non-AST nodes that have an AST canonical twin. - for nid in node_set: + for nid in sorted(node_set): attrs = G.nodes[nid] if attrs.get("_origin") == "ast": continue # AST nodes are never ghosts @@ -497,7 +694,32 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat # fragment (e.g. an incremental update whose fragment references a symbol in a # file that was NOT re-extracted) still resolves to the migrated node instead # of dangling. Only fills gaps — never overrides a real node id. + # + # The old-stem form drops the extension and (for the file node itself) every + # directory but the immediate parent, so it collapses easily: "ping.h" and + # "ping.php" in different directories both alias to bare "ping". Collecting + # every candidate for an alias BEFORE committing any of them — and only + # committing when exactly one candidate claims it — keeps this a precise + # re-keying aid instead of a silent cross-file (and cross-language) merge. + # Without this, a dangling edge to a bare, deliberately-unscoped fallback id + # (e.g. the C/C++ extractor's last-resort target for an #include it couldn't + # resolve to a real path) could ride this alias onto whichever unrelated + # same-stem file happened to be inserted first into ``node_set`` — a Python + # set, so "first" is hash-order, not anything meaningful. + # + # A file node's OWN id is not always a clean ``new_stem`` prefix: when a + # same-directory ``.h``/``.cpp`` pair collides on their shared pre-extension + # id, _disambiguate_colliding_node_ids salts both apart into ids like + # ``tools_aolserver_utility_h_tools_aolserver_utility`` — which no longer + # string-prefixes cleanly for the suffix math below. Detecting "this IS the + # file node" by label (every file node's label is its own basename, + # regardless of id mangling) instead of by id shape keeps a salted file node + # in the alias competition, so a genuine collision (a C header AND an + # unrelated same-named PHP script) is still caught as ambiguous instead of + # the header silently dropping out of the race and leaving the PHP file as + # the lone (wrong) "unambiguous" winner. from graphify.extractors.base import _file_stem as _fs + _alias_candidates: dict[str, set[str]] = {} for nid in node_set: attrs = G.nodes[nid] sf = attrs.get("source_file") @@ -507,15 +729,21 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat if rel.is_absolute(): continue new_stem = make_id(_fs(rel)) - suffix = "" - if _normalize_id(nid).startswith(new_stem): - suffix = _normalize_id(nid)[len(new_stem):] # leading "_entity" or "" + if str(attrs.get("label", "")) == rel.name: + suffix = "" # this node IS the file, whatever its (possibly salted) id + else: + suffix = "" + if _normalize_id(nid).startswith(new_stem): + suffix = _normalize_id(nid)[len(new_stem):] # leading "_entity" or "" for old_stem in _old_file_stems(rel): if old_stem == new_stem: continue alias = old_stem + suffix - norm_to_id.setdefault(_normalize_id(alias), nid) - norm_to_id.setdefault(alias, nid) + _alias_candidates.setdefault(_normalize_id(alias), set()).add(nid) + _alias_candidates.setdefault(alias, set()).add(nid) + for alias_key, candidates in _alias_candidates.items(): + if len(candidates) == 1: + norm_to_id.setdefault(alias_key, next(iter(candidates))) # Iterate edges in a deterministic order. The graph is undirected and stores # direction in _src/_tgt; when two edges collapse onto the same node pair the # last write wins, so an unstable iteration order flips _src/_tgt run-to-run @@ -567,24 +795,31 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat ) if "source_file" in attrs: attrs["source_file"] = _norm_source_file(attrs["source_file"], _root) - # Drop cross-language INFERRED `calls` edges — same short names (render, - # parse, etc.) appear across language boundaries in multi-language chunks, - # producing phantom edges that don't represent real call relationships. - if attrs.get("relation") == "calls" and attrs.get("confidence") == "INFERRED": - _LANG_FAMILY: dict[str, str] = { - ".py": "py", ".pyi": "py", - ".js": "js", ".mjs": "js", ".cjs": "js", ".jsx": "js", - ".ts": "js", ".tsx": "js", - ".go": "go", ".rs": "rs", - ".java": "jvm", ".kt": "jvm", ".scala": "jvm", ".groovy": "jvm", - ".c": "c", ".h": "c", ".cc": "cpp", ".cpp": "cpp", ".hpp": "cpp", - ".cu": "cpp", ".cuh": "cpp", ".metal": "cpp", - ".rb": "rb", ".php": "php", ".cs": "cs", ".swift": "swift", ".lua": "lua", - } + # Drop cross-language phantom edges — the same short names (render, parse, + # time, ...) recur across language boundaries, so an unresolved target can + # bind to a same-named node in another language. The extraction spec forbids + # this for `calls`; it is equally invalid for `imports`/`references` (a + # Python `import time` must not bind to a `time.ts`, #1749). + _edge_rel = attrs.get("relation") + if _edge_rel in ("calls", "imports", "imports_from", "references"): src_ext = Path(G.nodes[src].get("source_file") or "").suffix.lower() tgt_ext = Path(G.nodes[tgt].get("source_file") or "").suffix.lower() - if src_ext and tgt_ext and _LANG_FAMILY.get(src_ext) != _LANG_FAMILY.get(tgt_ext): - continue + src_fam = _EDGE_LANG_FAMILY.get(src_ext) + tgt_fam = _EDGE_LANG_FAMILY.get(tgt_ext) + if _edge_rel == "calls": + # Unchanged #1547/#1556 behavior: only INFERRED calls, and drop as + # soon as either family differs (an unknown ext counts as different). + if ( + attrs.get("confidence") == "INFERRED" + and src_ext and tgt_ext and src_fam != tgt_fam + ): + continue + else: + # imports/references: drop only when BOTH endpoints are known code + # languages of different families, so a config->code reference + # (unknown ext, e.g. a manifest) is never mistaken for a phantom. + if src_fam is not None and tgt_fam is not None and src_fam != tgt_fam: + continue # Preserve original edge direction - undirected graphs lose it otherwise, # causing display functions to show edges backwards. attrs["_src"] = src @@ -665,7 +900,15 @@ def deduplicate_by_label(nodes: list[dict], edges: list[dict]) -> tuple[list[dic """Merge nodes that share a normalised label, rewriting edge references. Prefers IDs without chunk suffixes (_c\\d+) and shorter IDs when tied. - Drops self-loops created by the merge. Called in build() automatically. + Drops self-loops created by the merge. + + Dormant: this is NOT wired into ``build()`` — the active dedup path is + ``deduplicate_entities`` (imported and called in ``build``), which supersedes + it. The previous "Called in build() automatically" note was never true. It + also merges by label alone with no ``file_type`` guard, so it must not be + enabled for code nodes: same-label symbols from different files/packages + (e.g. two ``Account`` types) would collapse into one — the cross-file + conflation ``deduplicate_entities`` deliberately avoids for code (#1205). """ _CHUNK_SUFFIX = re.compile(r"_c\d+$") canonical: dict[str, dict] = {} # norm_label -> surviving node @@ -737,16 +980,36 @@ def build_merge( # NetworkX round-trip loses direction permanently (#760). from graphify.security import check_graph_file_size_cap check_graph_file_size_cap(graph_path) - data = json.loads(graph_path.read_text(encoding="utf-8")) + try: + data = json.loads(graph_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError) as exc: + raise RuntimeError( + f"Cannot read {graph_path} for incremental merge: {exc}. " + "Delete the file and run a full rebuild." + ) from exc links_key = "links" if "links" in data else "edges" existing_nodes = list(data.get("nodes", [])) existing_edges = list(data.get(links_key, [])) + existing_hyperedges = list(data.get("hyperedges", [])) had_graph = True else: existing_nodes = [] existing_edges = [] + existing_hyperedges = [] had_graph = False + # Effective root for relativizing absolute source_file / prune paths back to the + # stored relative source_file keys. When the caller passes root we use it; + # otherwise fall back to the graph's recorded scan root, so absolute + # prune_sources and new-chunk paths still match even when a caller omits root + # (#1571 — the skill's --update runbook calls build_merge without root, so + # absolute deleted-file paths never matched the relative node keys and their + # nodes survived as ghosts). + _eff_root = ( + str(Path(root).resolve()) if root is not None + else _infer_merge_root(graph_path) + ) + # Re-extracted files REPLACE their prior contribution. Every source_file # present in new_chunks is dropped from the loaded base before merging, so a # CHANGED file's stale nodes/edges don't accumulate across incremental @@ -756,7 +1019,7 @@ def build_merge( # for them; genuinely deleted files are still handled via prune_sources. # Matched in both raw and _norm_source_file form because new_chunks may carry # absolute win32 paths while the stored graph keeps relative posix (#1007). - _replace_root = str(Path(root).resolve()) if root is not None else None + _replace_root = _eff_root new_sources: set[str] = set() for ch in new_chunks: for n in ch.get("nodes", []): @@ -779,23 +1042,53 @@ def _kept(item: dict) -> bool: all_chunks = base + list(new_chunks) G = build(all_chunks, directed=directed, dedup=dedup, dedup_llm_backend=dedup_llm_backend, root=root) + # Prune set for deleted source files — both the raw form (matches nodes that + # kept absolute source_file) and the normalised relative form (matches nodes + # relativised by _norm_source_file at build time). .resolve() (via _eff_root) + # handles symlinked roots and ".." / "./" segments so Path.relative_to() + # succeeds even when the scan root is a symlink. (#1007, #1571) + prune_set: set[str] = set() + for p in (prune_sources or []): + if not p: + continue + prune_set.add(p) + norm = _norm_source_file(p, _eff_root) + if norm: + prune_set.add(norm) + # A file that was just re-extracted (present in new_chunks) is being REPLACED, + # never deleted — so never prune it, even if the caller also lists it in + # prune_sources. Otherwise its fresh, just-built nodes are silently removed + # (data loss): common when an edit keeps a node's label and the caller follows + # the old edit-workflow of passing the changed file in prune_sources (#1796). + # "replace" wins over a contradictory "delete" of the same source. + prune_set -= new_sources + + # Carry forward hyperedges from files that were neither re-extracted nor + # deleted (#1574). build() only sees the new chunks' hyperedges, so without + # this every --update collapses the graph's hyperedge set down to just the + # changed files'. Re-extracted files' prior hyperedges are dropped (their new + # version is already in G — replace-per-source, like nodes/edges); deleted + # files' are dropped via prune_set. id-dedup (attach_hyperedges) so a carried + # hyperedge never duplicates one the new chunks re-emitted. Mirrors watch.py, + # which already preserves existing hyperedges across a rebuild. + if existing_hyperedges: + carried = [] + for he in existing_hyperedges: + if not isinstance(he, dict): + continue + sf = he.get("source_file") + norm = _norm_source_file(sf, _eff_root) + if sf in new_sources or norm in new_sources: + continue # re-extracted — replaced by the new chunk's version + if sf in prune_set or norm in prune_set: + continue # deleted — pruned + carried.append(he) + if carried: + from graphify.export import attach_hyperedges + attach_hyperedges(G, carried) + # Prune nodes and edges from deleted source files if prune_sources: - # Build a set containing both the raw form (matches nodes that kept - # absolute source_file) and the normalised relative form (matches nodes - # that were relativised by _norm_source_file at build time). - # .resolve() handles symlinked roots and redundant ".." / "./" segments - # so Path.relative_to() succeeds even when the scan root is a symlink. - # (#1007: manifest absolute paths vs graph relative source_file mismatch) - _root_str = str(Path(root).resolve()) if root is not None else None - prune_set: set[str] = set() - for p in prune_sources: - if not p: - continue - prune_set.add(p) - norm = _norm_source_file(p, _root_str) - if norm: - prune_set.add(norm) to_remove = [ n for n, d in G.nodes(data=True) if d.get("source_file") in prune_set @@ -857,6 +1150,34 @@ def prefix_graph_for_global(G: nx.Graph, repo_tag: str) -> nx.Graph: return H +def distinct_repo_tags(graph_paths: "list[Path]") -> "list[str]": + """Return a unique, human-meaningful repo tag per input graph for merge-graphs. + + The naive tag (the ``graphify-out`` parent dir name) is NOT unique across + inputs: ``src/graphify-out`` and ``frontend/src/graphify-out`` both yield + ``src``. Prefixing both node sets with ``src::`` then makes same-stem nodes + (a backend ``src/app.js`` and a frontend ``App.jsx``, both bare ``app``) + collide, so ``nx.compose`` silently merges two unrelated entities and invents + cross-runtime edges (#1729). Colliding tags are widened with their own parent + dir (``frontend_src``), then an index suffix guarantees uniqueness so no two + graphs ever share a prefix. + """ + repo_dirs = [p.parent.parent for p in graph_paths] # graphify-out/.. → repo dir + tags = [d.name or "repo" for d in repo_dirs] + if len(set(tags)) != len(tags): + widened: list[str] = [] + for d in repo_dirs: + parent = d.parent.name + widened.append(f"{parent}_{d.name}" if parent and d.name else (d.name or "repo")) + tags = widened + seen: dict[str, int] = {} + unique: list[str] = [] + for t in tags: + seen[t] = seen.get(t, 0) + 1 + unique.append(t if seen[t] == 1 else f"{t}-{seen[t]}") + return unique + + def prune_repo_from_graph(G: nx.Graph, repo_tag: str) -> int: """Remove all nodes tagged with repo_tag from G in-place. Returns count removed.""" to_remove = [n for n, d in G.nodes(data=True) if d.get("repo") == repo_tag] diff --git a/graphify/cache.py b/graphify/cache.py index f377889ff..1a7d42d06 100644 --- a/graphify/cache.py +++ b/graphify/cache.py @@ -7,6 +7,8 @@ import os import re import tempfile +import warnings +from collections.abc import Iterable from pathlib import Path # Output directory name — override with GRAPHIFY_OUT env var for worktrees or @@ -100,11 +102,15 @@ def _stat_index_file(root: Path) -> Path: return base / "cache" / "stat-index.json" -def _ensure_stat_index(root: Path) -> None: +def _ensure_stat_index(root: Path, cache_root: "Path | None" = None) -> None: global _stat_index, _stat_index_root, _stat_index_dirty if _stat_index_root is not None: return - _stat_index_root = Path(root).resolve() + # The stat index only determines the cache FILE location (entry keys are + # absolute paths), so honoring an explicit cache_root keeps detect()'s + # word-count cache under the requested --out dir instead of polluting the + # scanned corpus with a stray graphify-out/ (#1747). + _stat_index_root = Path(cache_root if cache_root is not None else root).resolve() p = _stat_index_file(_stat_index_root) if p.exists(): try: @@ -153,7 +159,7 @@ def _normalize_path(path: Path) -> Path: return Path(os.path.normcase(s)) -def file_hash(path: Path, root: Path = Path(".")) -> str: +def file_hash(path: Path, root: Path = Path("."), cache_root: "Path | None" = None) -> str: """SHA256 of file contents + path relative to root. Uses a stat-based fastpath (size + mtime_ns) to skip full reads when the @@ -173,13 +179,18 @@ def file_hash(path: Path, root: Path = Path(".")) -> str: if not p.is_file(): raise IsADirectoryError(f"file_hash requires a file, got: {p}") - _ensure_stat_index(root) + # The stat index is a cache artifact, so it must follow the cache location + # (cache_root), not the key-anchor root — otherwise it leaves a stray + # graphify-out/cache/stat-index.json inside the analyzed source tree even when + # the AST cache itself is redirected to CWD (#1774 completion). + _ensure_stat_index(root, cache_root=cache_root) abs_key = str(p.resolve()) st: "os.stat_result | None" = None try: st = p.stat() entry = _stat_index.get(abs_key) if (entry + and entry.get("hash") is not None # word-count-only entries carry no hash and entry.get("size") == st.st_size and entry.get("mtime_ns") == st.st_mtime_ns): return entry["hash"] @@ -199,12 +210,65 @@ def file_hash(path: Path, root: Path = Path(".")) -> str: digest = h.hexdigest() if st is not None: - _stat_index[abs_key] = {"size": st.st_size, "mtime_ns": st.st_mtime_ns, "hash": digest} + entry = _stat_index.get(abs_key) + if (entry is not None + and entry.get("size") == st.st_size + and entry.get("mtime_ns") == st.st_mtime_ns): + entry["hash"] = digest # preserve a co-located word_count + else: + _stat_index[abs_key] = {"size": st.st_size, "mtime_ns": st.st_mtime_ns, "hash": digest} _stat_index_dirty = True return digest +def cached_word_count(path: Path, root: Path, compute, cache_root: "Path | None" = None) -> int: + """Word count with the same (size, mtime_ns) stat-fastpath cache as + :func:`file_hash`, persisted in the shared stat index. + + ``detect()`` counts words in every PDF/docx/text file to size the corpus, + which re-opens and re-parses every binary on each run — minutes on a large + docs corpus even when only a handful of files changed (#1656). This caches + the count against the file's stat signature so an unchanged file is counted + once and read from the index thereafter. ``compute(path)`` produces the + count on a miss. A file that can't be stat'd (e.g. a Windows long path the + index normalization can't reach) simply recomputes and isn't cached — + correct, just not accelerated. + """ + global _stat_index_dirty + p = _normalize_path(Path(path)) + root = _normalize_path(Path(root)) + _ensure_stat_index(root, cache_root=cache_root) + abs_key = str(p.resolve()) + st: "os.stat_result | None" = None + try: + st = p.stat() + entry = _stat_index.get(abs_key) + if (entry + and entry.get("size") == st.st_size + and entry.get("mtime_ns") == st.st_mtime_ns + and "word_count" in entry): + return entry["word_count"] + except OSError: + pass + + wc = compute(Path(path)) + + if st is not None: + entry = _stat_index.get(abs_key) + if (entry + and entry.get("size") == st.st_size + and entry.get("mtime_ns") == st.st_mtime_ns): + entry["word_count"] = wc # augment the existing hash entry in place + else: + _stat_index[abs_key] = { + "size": st.st_size, "mtime_ns": st.st_mtime_ns, "word_count": wc, + } + _stat_index_dirty = True + + return wc + + def _relativize_source_files_in(payload: dict, root: Path) -> None: """Mutate ``payload`` to rewrite absolute ``source_file`` fields as forward-slash relative paths from ``root``. @@ -222,7 +286,11 @@ def _relativize_source_files_in(payload: dict, root: Path) -> None: root_resolved = Path(root).resolve() except OSError: return - for bucket in ("nodes", "edges", "hyperedges"): + # raw_calls (#: Pascal/Delphi cross-file inherited-call resolution) carries + # source_file the same way nodes/edges/hyperedges do, so it needs the same + # portable-path treatment for cache entries to round-trip correctly across + # machines/checkout directories. + for bucket in ("nodes", "edges", "hyperedges", "raw_calls"): for item in payload.get(bucket, []): if not isinstance(item, dict): continue @@ -253,7 +321,7 @@ def _absolutize_source_files_in(payload: dict, root: Path) -> None: root_resolved = Path(root).resolve() except OSError: return - for bucket in ("nodes", "edges", "hyperedges"): + for bucket in ("nodes", "edges", "hyperedges", "raw_calls"): for item in payload.get(bucket, []): if not isinstance(item, dict): continue @@ -290,24 +358,33 @@ def cache_dir(root: Path = Path("."), kind: str = "ast") -> Path: return d -def load_cached(path: Path, root: Path = Path("."), kind: str = "ast") -> dict | None: +def load_cached(path: Path, root: Path = Path("."), kind: str = "ast", + cache_root: Path | None = None) -> dict | None: """Return cached extraction for this file if hash matches, else None. Cache key: SHA256 of file contents. Cache value: stored as graphify-out/cache/{kind}/{hash}.json (AST entries under the per-version subdirectory, see :func:`cache_dir`). + ``root`` anchors the content-hash key and source_file relativization (it + must stay the inferred common parent so keys remain portable). ``cache_root`` + decouples *where* the cache directory lives from that anchor — the cache is + an output and must not land inside a read-only/analyzed source tree (#1774). + When ``cache_root`` is None the location falls back to ``root`` (unchanged + behavior for existing callers). + AST entries written by other graphify versions — including the legacy flat cache/ layout (pre-0.5.3) and the unversioned cache/ast/ layout — are deliberately not consulted: they were produced by a different extractor and may be stale. Returns None if no cache entry or file has changed. """ + location = cache_root if cache_root is not None else root try: - h = file_hash(path, root) + h = file_hash(path, root, cache_root=cache_root) except OSError: return None - entry = cache_dir(root, kind) / f"{h}.json" + entry = cache_dir(location, kind) / f"{h}.json" if entry.exists(): try: result = json.loads(entry.read_text(encoding="utf-8")) @@ -322,12 +399,17 @@ def load_cached(path: Path, root: Path = Path("."), kind: str = "ast") -> dict | return None -def save_cached(path: Path, result: dict, root: Path = Path("."), kind: str = "ast") -> None: +def save_cached(path: Path, result: dict, root: Path = Path("."), kind: str = "ast", + cache_root: Path | None = None) -> None: """Save extraction result for this file. Stores as graphify-out/cache/{kind}/{hash}.json where hash = SHA256 of current file contents. result should be a dict with 'nodes' and 'edges' lists. + ``root`` anchors the content-hash key and source_file relativization; + ``cache_root`` (when given) is where the cache directory is written, decoupled + from ``root`` so the cache never lands inside the analyzed source tree (#1774). + No-ops if `path` is not a regular file. Subagent-produced semantic fragments occasionally carry a directory path in `source_file`; skipping them prevents IsADirectoryError from aborting the whole batch. @@ -346,12 +428,13 @@ def save_cached(path: Path, result: dict, root: Path = Path("."), kind: str = "a # source_file field's original absolute form. Mutating the input here would # silently break those remaps on the first extraction pass. on_disk = result - if isinstance(result, dict) and any(result.get(k) for k in ("nodes", "edges", "hyperedges")): + if isinstance(result, dict) and any(result.get(k) for k in ("nodes", "edges", "hyperedges", "raw_calls")): import copy as _copy on_disk = _copy.deepcopy(result) _relativize_source_files_in(on_disk, root) - h = file_hash(p, root) - target_dir = cache_dir(root, kind) + h = file_hash(p, root, cache_root=cache_root) + location = cache_root if cache_root is not None else root + target_dir = cache_dir(location, kind) entry = target_dir / f"{h}.json" fd, tmp_path = tempfile.mkstemp(dir=target_dir, prefix=f"{h}.", suffix=".tmp") try: @@ -479,12 +562,24 @@ def save_semantic_cache( edges: list[dict], hyperedges: list[dict] | None = None, root: Path = Path("."), + merge_existing: bool = False, + allowed_source_files: Iterable[str | Path] | None = None, ) -> int: """Save semantic extraction results to cache, keyed by source_file. Groups nodes and edges by source_file, then saves one cache entry per file under cache/semantic/ (separate from AST entries in cache/ast/) to prevent hash-key collisions (#582). + + When ``merge_existing`` is True, any already-cached entry for a file is + unioned with the new results before saving instead of being overwritten. + This lets callers checkpoint incrementally (e.g. once per chunk) without + dropping a prior slice of a large file that was split across chunks. + + When ``allowed_source_files`` is provided, only those files may be used as + cache-write keys. Semantic nodes can legitimately mention another corpus + file, but a model must not be able to replace that file's complete cache + entry unless the file was part of the current extraction batch (#1757). Returns the number of files cached. """ from collections import defaultdict @@ -503,12 +598,43 @@ def save_semantic_cache( if src: by_file[src]["hyperedges"].append(h) + root_path = Path(root).resolve() + + def resolved_source_path(value: str | Path) -> Path: + path = Path(value) + if not path.is_absolute(): + path = root_path / path + try: + return path.resolve() + except (OSError, RuntimeError): + # Keep the cache write best-effort for inaccessible paths or a + # symlink loop emitted by an untrusted semantic result. + return Path(os.path.abspath(path)) + + allowed_paths = None + if allowed_source_files is not None: + allowed_paths = {resolved_source_path(path) for path in allowed_source_files} + saved = 0 for fpath, result in by_file.items(): - p = Path(fpath) - if not p.is_absolute(): - p = Path(root) / p + p = resolved_source_path(fpath) if p.is_file(): + if allowed_paths is not None and p not in allowed_paths: + warnings.warn( + "semantic cache skipped out-of-scope source_file " + f"{fpath!r}; the file was not dispatched for extraction", + RuntimeWarning, + stacklevel=2, + ) + continue + if merge_existing: + prev = load_cached(p, root, kind="semantic") + if prev: + result = { + "nodes": (prev.get("nodes", []) or []) + result["nodes"], + "edges": (prev.get("edges", []) or []) + result["edges"], + "hyperedges": (prev.get("hyperedges", []) or []) + result["hyperedges"], + } save_cached(p, result, root, kind="semantic") saved += 1 return saved diff --git a/graphify/cli.py b/graphify/cli.py new file mode 100644 index 000000000..c9104912d --- /dev/null +++ b/graphify/cli.py @@ -0,0 +1,2789 @@ +"""graphify command dispatch — every non-install subcommand. + +Extracted verbatim from __main__.main(); __main__ now calls dispatch_command(cmd) +after the install/platform dispatch. Kept out of __main__ to shrink the CLI entry +module. The path-redirect (`graphify ` -> extract) re-enters via a lazy +import of main to avoid a cli<->__main__ import cycle. +""" +from __future__ import annotations +import json +import os +import sys +from graphify.paths import GRAPHIFY_OUT as _GRAPHIFY_OUT +from pathlib import Path + + +_SEARCH_NUDGE = json.dumps({ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "additionalContext": ( + 'MANDATORY: graphify-out/graph.json exists. You MUST run ' + '`graphify query ""` before grepping raw files. Only grep ' + 'after graphify has oriented you, or to modify/debug specific lines.' + ), + } +}, ensure_ascii=False, separators=(",", ":")) + "\n" +_READ_NUDGE = json.dumps({ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "additionalContext": ( + 'MANDATORY: graphify-out/graph.json exists. You MUST run graphify ' + 'before reading source files. Use: `graphify query ""` ' + '(scoped subgraph), `graphify explain ""`, or ' + '`graphify path "" ""`. Only read raw files after graphify has ' + 'oriented you, or to modify/debug specific lines. This rule applies to ' + 'subagents too — include it in every subagent prompt involving code ' + 'exploration.' + ), + } +}, ensure_ascii=False, separators=(",", ":")) + "\n" +_HOOK_SOURCE_EXTS = ( + '.py', '.js', '.ts', '.tsx', '.jsx', '.astro', '.vue', '.svelte', '.go', + '.rs', '.java', '.rb', '.c', '.h', '.cpp', '.hpp', '.cc', '.cs', '.kt', + '.swift', '.php', '.scala', '.lua', '.sh', '.md', '.rst', '.txt', '.mdx', +) +_GEMINI_NUDGE_TEXT = ( + 'graphify: knowledge graph at graphify-out/. For focused questions, run ' + '`graphify query ""` (scoped subgraph, usually much smaller than ' + 'GRAPH_REPORT.md) instead of grepping raw files. Read GRAPH_REPORT.md only ' + 'for broad architecture context.' +) + + +def _default_graph_path() -> str: + return str(Path(_GRAPHIFY_OUT) / "graph.json") +class _StageTimer: + """Print per-stage wall-clock timings to stderr when --timing is set (#1490). + + Monotonic (perf_counter), diagnostic-only: emits ``[graphify timing] : + N.Ns`` after each stage and a final total. Off by default, so normal output is + byte-identical and machine-read stdout is untouched. + """ + + def __init__(self, enabled: bool) -> None: + import time as _time + self._now = _time.perf_counter + self.enabled = enabled + self.start = self._now() + self._last = self.start + + def mark(self, stage: str) -> None: + now = self._now() + if self.enabled: + print(f"[graphify timing] {stage}: {now - self._last:.1f}s", file=sys.stderr) + self._last = now + + def total(self) -> None: + if self.enabled: + print(f"[graphify timing] total: {self._now() - self.start:.1f}s", file=sys.stderr) +def _enforce_graph_size_cap_or_exit(gp: Path) -> None: + """Reject oversized graph files before parsing (CLI exit-on-fail flavor). + + Delegates to ``graphify.security.check_graph_file_size_cap`` and turns the + raised ``ValueError`` into a CLI-style ``error: ...`` message + exit 1. + Use this from ``__main__.py`` subcommands that already use the ``print + + sys.exit(1)`` idiom. Library/MCP/loader callers (``serve._load_graph``, + ``build``, ``benchmark``, ``tree_html``, ``callflow_html``, ``prs``, + ``global_graph``, ``watch``, ``export``) call the security helper directly + and let the ``ValueError`` propagate. + """ + from graphify.security import check_graph_file_size_cap + try: + check_graph_file_size_cap(gp) + except ValueError as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(1) +def _run_hook_guard(kind: str) -> None: + """Shell-agnostic PreToolUse guard (#522). + + Reads the tool-call JSON from stdin and, when a knowledge graph exists in the + current output dir, prints a nudge (`additionalContext`) telling the agent to + use graphify instead of grepping/reading raw files. Replaces the old inline + bash hooks that failed to parse on Windows. Always fails open: any error, or a + non-matching tool call, prints nothing and the caller exits 0, so a legitimate + tool call is never blocked. Detection mirrors the previous hooks exactly. + """ + from graphify.paths import out_path, GRAPHIFY_OUT_NAME + # Gemini's BeforeTool hook takes no stdin and must ALWAYS return a decision so + # the tool is never blocked; the graph nudge is appended only when a graph + # exists. Handled before the stdin read below (which the search/read guards need). + if kind == "gemini": + payload = {"decision": "allow"} + try: + if out_path("graph.json").is_file(): + payload["additionalContext"] = _GEMINI_NUDGE_TEXT + except Exception: + pass + sys.stdout.write(json.dumps(payload, ensure_ascii=False, separators=(",", ":"))) + return + try: + d = json.loads(sys.stdin.buffer.read().decode("utf-8", "replace")) + except Exception: + return + if not isinstance(d, dict): + return + t = d.get("tool_input", d) + if not isinstance(t, dict): + return + try: + if kind == "search": + cmd_str = str(t.get("command", "") or "") + # Same set the old `case` matched: *grep*, *ripgrep*, and rg/find/fd/ + # ack/ag as a token (name followed by a space). + if any(tok in cmd_str for tok in ("grep", "ripgrep", "rg ", "find ", "fd ", "ack ", "ag ")) \ + and out_path("graph.json").is_file(): + sys.stdout.write(_SEARCH_NUDGE) + elif kind == "read": + vals = [str(t.get("file_path") or ""), str(t.get("pattern") or ""), str(t.get("path") or "")] + j = " ".join(vals).lower().replace("\\", "/") + tails = [ + "." + seg.rsplit(".", 1)[-1] + for v in vals if v + for seg in [v.lower().replace("\\", "/").rsplit("/", 1)[-1]] + if "." in seg + ] + under_out = "graphify-out/" in j or (GRAPHIFY_OUT_NAME.lower() + "/") in j + if not under_out and any(tl in _HOOK_SOURCE_EXTS for tl in tails) \ + and out_path("graph.json").is_file(): + sys.stdout.write(_READ_NUDGE) + except Exception: + pass +def _clone_repo( + url: str, branch: str | None = None, out_dir: Path | None = None +) -> Path: + """Clone a GitHub repo to a local cache dir and return the path. + + Clones into ~/.graphify/repos// by default so repeated + runs on the same URL reuse the existing clone (git pull instead of clone). + """ + import subprocess as _sp + import re as _re + + # Normalise URL — strip trailing .git if present + url = url.rstrip("/") + if not url.endswith(".git"): + git_url = url + ".git" + else: + git_url = url + url = url[:-4] + + # Extract owner/repo from URL + m = _re.search(r"github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$", url) + if not m: + print(f"error: not a recognised GitHub URL: {url}", file=sys.stderr) + sys.exit(1) + owner, repo = m.group(1), m.group(2) + + if out_dir: + dest = out_dir + else: + dest = Path.home() / ".graphify" / "repos" / owner / repo + + if branch and branch.startswith("-"): + print(f"error: invalid branch name: {branch!r}", file=sys.stderr) + sys.exit(1) + + if dest.exists(): + print(f"Repo already cloned at {dest} - pulling latest...", flush=True) + cmd = ["git", "-C", str(dest), "pull"] + if branch: + cmd += ["origin", "--", branch] + result = _sp.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"warning: git pull failed:\n{result.stderr}", file=sys.stderr) + else: + dest.parent.mkdir(parents=True, exist_ok=True) + print(f"Cloning {url} -> {dest} ...", flush=True) + cmd = ["git", "clone", "--depth", "1"] + if branch: + cmd += ["--branch", branch] + cmd += ["--", git_url, str(dest)] + result = _sp.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"error: git clone failed:\n{result.stderr}", file=sys.stderr) + sys.exit(1) + + print(f"Ready at: {dest}", flush=True) + return dest + + +def _reenter_main() -> None: + from graphify.__main__ import main + main() + + +def dispatch_command(cmd: str) -> None: + if cmd == "provider": + from graphify.llm import _custom_providers_path, BACKENDS + import json as _json + subcmd = sys.argv[2] if len(sys.argv) > 2 else "" + global_path = _custom_providers_path(global_=True) + + if subcmd == "list": + global_path.parent.mkdir(parents=True, exist_ok=True) + existing: dict = {} + if global_path.is_file(): + try: + existing = _json.loads(global_path.read_text(encoding="utf-8")) + except Exception: + pass + if not existing: + print("No custom providers registered.") + else: + for name in existing: + print(f" {name} ({existing[name].get('base_url', '')})") + + elif subcmd == "show": + name = sys.argv[3] if len(sys.argv) > 3 else "" + if not name: + print("Usage: graphify provider show ", file=sys.stderr) + sys.exit(1) + existing = {} + if global_path.is_file(): + try: + existing = _json.loads(global_path.read_text(encoding="utf-8")) + except Exception: + pass + if name not in existing: + print(f"Provider '{name}' not found.", file=sys.stderr) + sys.exit(1) + print(_json.dumps({name: existing[name]}, indent=2)) + + elif subcmd == "add": + args = sys.argv[3:] + name = args[0] if args and not args[0].startswith("-") else "" + if not name: + print("Usage: graphify provider add --base-url URL --default-model MODEL --env-key KEY", file=sys.stderr) + sys.exit(1) + if name in BACKENDS: + print(f"Error: '{name}' is a built-in provider and cannot be overridden.", file=sys.stderr) + sys.exit(1) + base_url = "" + default_model = "" + env_key = "" + pricing_input = 0.0 + pricing_output = 0.0 + i = 1 + while i < len(args): + a = args[i] + if a == "--base-url" and i + 1 < len(args): + base_url = args[i + 1]; i += 2 + elif a.startswith("--base-url="): + base_url = a.split("=", 1)[1]; i += 1 + elif a == "--default-model" and i + 1 < len(args): + default_model = args[i + 1]; i += 2 + elif a.startswith("--default-model="): + default_model = a.split("=", 1)[1]; i += 1 + elif a == "--env-key" and i + 1 < len(args): + env_key = args[i + 1]; i += 2 + elif a.startswith("--env-key="): + env_key = a.split("=", 1)[1]; i += 1 + elif a == "--pricing-input" and i + 1 < len(args): + pricing_input = float(args[i + 1]); i += 2 + elif a == "--pricing-output" and i + 1 < len(args): + pricing_output = float(args[i + 1]); i += 2 + else: + i += 1 + if not base_url or not default_model or not env_key: + print("Error: --base-url, --default-model, and --env-key are required.", file=sys.stderr) + sys.exit(1) + from graphify.llm import provider_base_url_ok + if not provider_base_url_ok(base_url, name): + print(f"Error: refusing to add provider with unsafe base_url {base_url!r}.", file=sys.stderr) + sys.exit(1) + global_path.parent.mkdir(parents=True, exist_ok=True) + existing = {} + if global_path.is_file(): + try: + existing = _json.loads(global_path.read_text(encoding="utf-8")) + except Exception: + pass + existing[name] = { + "base_url": base_url, + "default_model": default_model, + "env_key": env_key, + "pricing": {"input": pricing_input, "output": pricing_output}, + "temperature": 0, + } + global_path.write_text(_json.dumps(existing, indent=2) + "\n", encoding="utf-8") + print(f"Provider '{name}' added. Use with: graphify extract . --backend {name}") + + elif subcmd == "remove": + name = sys.argv[3] if len(sys.argv) > 3 else "" + if not name: + print("Usage: graphify provider remove ", file=sys.stderr) + sys.exit(1) + existing = {} + if global_path.is_file(): + try: + existing = _json.loads(global_path.read_text(encoding="utf-8")) + except Exception: + pass + if name not in existing: + print(f"Provider '{name}' not found.", file=sys.stderr) + sys.exit(1) + del existing[name] + global_path.write_text(_json.dumps(existing, indent=2) + "\n", encoding="utf-8") + print(f"Provider '{name}' removed.") + + else: + print("Usage: graphify provider [add|list|show|remove]", file=sys.stderr) + if subcmd: + sys.exit(1) + elif cmd == "prs": + from graphify.prs import cmd_prs + cmd_prs(sys.argv[2:]) + elif cmd == "hook": + from graphify.hooks import ( + install as hook_install, + uninstall as hook_uninstall, + status as hook_status, + ) + + subcmd = sys.argv[2] if len(sys.argv) > 2 else "" + if subcmd == "install": + print(hook_install(Path("."))) + elif subcmd == "uninstall": + print(hook_uninstall(Path("."))) + elif subcmd == "status": + print(hook_status(Path("."))) + else: + print("Usage: graphify hook [install|uninstall|status]", file=sys.stderr) + sys.exit(1) + elif cmd == "query": + if len(sys.argv) < 3: + print("Usage: graphify query \"\" [--dfs] [--context C] [--budget N] [--graph path]", file=sys.stderr) + sys.exit(1) + from graphify.serve import _query_graph_text + from graphify.security import sanitize_label + from networkx.readwrite import json_graph + from graphify import querylog + + question = sys.argv[2] + use_dfs = "--dfs" in sys.argv + budget = 2000 + graph_path = _default_graph_path() + context_filters: list[str] = [] + args = sys.argv[3:] + i = 0 + while i < len(args): + if args[i] == "--budget" and i + 1 < len(args): + try: + budget = int(args[i + 1]) + except ValueError: + print(f"error: --budget must be an integer", file=sys.stderr) + sys.exit(1) + i += 2 + elif args[i].startswith("--budget="): + try: + budget = int(args[i].split("=", 1)[1]) + except ValueError: + print(f"error: --budget must be an integer", file=sys.stderr) + sys.exit(1) + i += 1 + elif args[i] == "--context" and i + 1 < len(args): + context_filters.append(args[i + 1]) + i += 2 + elif args[i].startswith("--context="): + context_filters.append(args[i].split("=", 1)[1]) + i += 1 + elif args[i] == "--graph" and i + 1 < len(args): + graph_path = args[i + 1] + i += 2 + else: + i += 1 + gp = Path(graph_path).resolve() + if not gp.exists(): + print(f"error: graph file not found: {gp}", file=sys.stderr) + sys.exit(1) + if not gp.suffix == ".json": + print(f"error: graph file must be a .json file", file=sys.stderr) + sys.exit(1) + _enforce_graph_size_cap_or_exit(gp) + try: + import json as _json + import networkx as _nx + + _raw = _json.loads(gp.read_text(encoding="utf-8")) + if "links" not in _raw and "edges" in _raw: + _raw = dict(_raw, links=_raw["edges"]) + try: + G = json_graph.node_link_graph(_raw, edges="links") + except TypeError: + G = json_graph.node_link_graph(_raw) + try: + from graphify.build import graph_has_legacy_ids as _legacy + if _legacy(_raw.get("nodes", [])): + print( + "[graphify] note: this graph uses the pre-#1504 node-ID scheme; " + "rebuild with `graphify extract --force` to get path-qualified IDs " + "(fixes same-name-file collisions).", + file=sys.stderr, + ) + except Exception: + pass + except Exception as exc: + print(f"error: could not load graph: {exc}", file=sys.stderr) + sys.exit(1) + import time as _time + _t0 = _time.perf_counter() + _mode = "dfs" if use_dfs else "bfs" + _result = _query_graph_text( + G, + question, + mode=_mode, + depth=2, + token_budget=budget, + context_filters=context_filters, + ) + querylog.log_query( + kind="query", + question=question, + corpus=str(gp), + result=_result, + mode=_mode, + depth=2, + token_budget=budget, + duration_ms=(_time.perf_counter() - _t0) * 1000, + ) + print(_result) + elif cmd == "affected": + if len(sys.argv) < 3: + print("Usage: graphify affected \"\" [--relation R] [--depth N] [--graph path]", file=sys.stderr) + sys.exit(1) + from graphify.affected import DEFAULT_AFFECTED_RELATIONS, format_affected, load_graph + query = sys.argv[2] + graph_path = _default_graph_path() + depth = 2 + relations: list[str] = [] + args = sys.argv[3:] + i = 0 + while i < len(args): + if args[i] == "--graph" and i + 1 < len(args): + graph_path = args[i + 1] + i += 2 + elif args[i].startswith("--graph="): + graph_path = args[i].split("=", 1)[1] + i += 1 + elif args[i] == "--depth" and i + 1 < len(args): + try: + depth = int(args[i + 1]) + except ValueError: + print("error: --depth must be an integer", file=sys.stderr) + sys.exit(1) + i += 2 + elif args[i].startswith("--depth="): + try: + depth = int(args[i].split("=", 1)[1]) + except ValueError: + print("error: --depth must be an integer", file=sys.stderr) + sys.exit(1) + i += 1 + elif args[i] == "--relation" and i + 1 < len(args): + relations.append(args[i + 1]) + i += 2 + elif args[i].startswith("--relation="): + relations.append(args[i].split("=", 1)[1]) + i += 1 + else: + i += 1 + gp = Path(graph_path).resolve() + if not gp.exists(): + print(f"error: graph file not found: {gp}", file=sys.stderr) + sys.exit(1) + if not gp.suffix == ".json": + print("error: graph file must be a .json file", file=sys.stderr) + sys.exit(1) + try: + graph = load_graph(gp) + except Exception as exc: + print(f"error: could not load graph: {exc}", file=sys.stderr) + sys.exit(1) + print( + format_affected( + graph, + query, + relations=relations or DEFAULT_AFFECTED_RELATIONS, + depth=depth, + ) + ) + elif cmd == "save-result": + # graphify save-result --question Q --answer A [--type T] [--nodes N1 N2 ...] + # [--outcome useful|dead_end|corrected] [--correction TEXT] + import argparse as _ap + + p = _ap.ArgumentParser(prog="graphify save-result") + p.add_argument("--question", required=True) + p.add_argument("--answer", default=None) + p.add_argument("--answer-file", dest="answer_file", default=None) + p.add_argument("--type", dest="query_type", default="query") + p.add_argument("--nodes", nargs="*", default=[]) + p.add_argument("--outcome", choices=("useful", "dead_end", "corrected"), default=None) + p.add_argument("--correction", default=None) + p.add_argument("--memory-dir", default=str(Path(_GRAPHIFY_OUT) / "memory")) + opts = p.parse_args(sys.argv[2:]) + if opts.answer_file: + opts.answer = Path(opts.answer_file).read_text(encoding="utf-8").strip() + elif not opts.answer: + p.error("--answer or --answer-file is required") + from graphify.ingest import save_query_result as _sqr + + out = _sqr( + question=opts.question, + answer=opts.answer, + memory_dir=Path(opts.memory_dir), + query_type=opts.query_type, + source_nodes=opts.nodes or None, + outcome=opts.outcome, + correction=opts.correction, + ) + print(f"Saved to {out}") + elif cmd == "reflect": + import argparse as _ap + + p = _ap.ArgumentParser(prog="graphify reflect") + p.add_argument("--memory-dir", default=str(Path(_GRAPHIFY_OUT) / "memory")) + p.add_argument( + "--out", + default=str(Path(_GRAPHIFY_OUT) / "reflections" / "LESSONS.md"), + ) + p.add_argument("--graph", default=None) + p.add_argument("--analysis", default=None) + p.add_argument("--labels", default=None) + p.add_argument("--half-life-days", type=float, default=30.0, + help="signal weight halves every N days (default 30)") + p.add_argument("--min-corroboration", type=int, default=2, + help="distinct useful results to promote a node to preferred (default 2)") + p.add_argument("--if-stale", action="store_true", + help="skip when LESSONS.md is already newer than every input " + "(e.g. the git hook just refreshed it)") + opts = p.parse_args(sys.argv[2:]) + from graphify.reflect import reflect as _reflect, lessons_fresh as _lessons_fresh + + graph_arg = opts.graph + if graph_arg is None: + default_graph = Path(_GRAPHIFY_OUT) / "graph.json" + if default_graph.exists(): + graph_arg = str(default_graph) + + _gp = Path(graph_arg) if graph_arg else None + _analysis_path = None + _labels_path = None + if _gp is not None: + _analysis_path = Path(opts.analysis) if opts.analysis else ( + _gp.parent / ".graphify_analysis.json") + _labels_path = Path(opts.labels) if opts.labels else ( + _gp.parent / ".graphify_labels.json") + + if opts.if_stale and _lessons_fresh( + Path(opts.out), Path(opts.memory_dir), _gp, _analysis_path, _labels_path + ): + print(f"Lessons already up to date -> {opts.out} (skipped; omit --if-stale to force)") + else: + out_path, agg = _reflect( + memory_dir=Path(opts.memory_dir), + out_path=Path(opts.out), + graph_path=_gp, + analysis_path=_analysis_path, + labels_path=_labels_path, + half_life_days=opts.half_life_days, + min_corroboration=opts.min_corroboration, + ) + c = agg["counts"] + print( + f"Reflected {agg['total']} memories " + f"({c['useful']} useful, {c['dead_end']} dead ends, " + f"{c['corrected']} corrected) -> {out_path}" + ) + elif cmd == "path": + if len(sys.argv) < 4: + print( + 'Usage: graphify path "" "" [--graph path]', + file=sys.stderr, + ) + sys.exit(1) + from graphify.serve import _pick_scored_endpoint, _score_nodes + from networkx.readwrite import json_graph + import networkx as _nx + + source_label = sys.argv[2] + target_label = sys.argv[3] + graph_path = _default_graph_path() + args = sys.argv[4:] + for i, a in enumerate(args): + if a == "--graph" and i + 1 < len(args): + graph_path = args[i + 1] + gp = Path(graph_path).resolve() + if not gp.exists(): + print(f"error: graph file not found: {gp}", file=sys.stderr) + sys.exit(1) + _enforce_graph_size_cap_or_exit(gp) + _raw = json.loads(gp.read_text(encoding="utf-8")) + if "links" not in _raw and "edges" in _raw: + _raw = dict(_raw, links=_raw["edges"]) + # Force directed so the renderer can recover stored caller→callee direction. + _raw = {**_raw, "directed": True} + try: + G = json_graph.node_link_graph(_raw, edges="links") + except TypeError: + G = json_graph.node_link_graph(_raw) + src_scored = _score_nodes(G, [t.lower() for t in source_label.split()]) + tgt_scored = _score_nodes(G, [t.lower() for t in target_label.split()]) + if not src_scored: + print(f"No node matching '{source_label}' found.", file=sys.stderr) + sys.exit(1) + if not tgt_scored: + print(f"No node matching '{target_label}' found.", file=sys.stderr) + sys.exit(1) + src_nid = _pick_scored_endpoint(G, src_scored, source_label) + tgt_nid = _pick_scored_endpoint(G, tgt_scored, target_label) + # Ambiguity guard: when both queries resolve to the same node, the + # shortest path is trivially zero hops, which is almost never what the + # caller wanted (see bug #828). + if src_nid == tgt_nid: + print( + f"'{source_label}' and '{target_label}' both resolved to the same " + f"node '{src_nid}'. Use a more specific label or the exact node ID.", + file=sys.stderr, + ) + sys.exit(1) + for _name, _scored, _nid in ( + ("source", src_scored, src_nid), + ("target", tgt_scored, tgt_nid), + ): + # A close runner-up only made the resolution ambiguous when the raw + # score head is what got picked; a full-token override was chosen on + # token coverage, not score, so the head's margin is irrelevant. + if len(_scored) >= 2 and _nid == _scored[0][1]: + _top, _runner = _scored[0][0], _scored[1][0] + if _top > 0 and (_top - _runner) / _top < 0.10: + print( + f"warning: {_name} match was ambiguous " + f"(top score {_top:g}, runner-up {_runner:g})", + file=sys.stderr, + ) + try: + path_nodes = _nx.shortest_path(G.to_undirected(as_view=True), src_nid, tgt_nid) + except (_nx.NetworkXNoPath, _nx.NodeNotFound): + print(f"No path found between '{source_label}' and '{target_label}'.") + sys.exit(0) + hops = len(path_nodes) - 1 + segments = [] + from graphify.build import edge_data + for i in range(len(path_nodes) - 1): + u, v = path_nodes[i], path_nodes[i + 1] + # Check which direction the stored edge points. + if G.has_edge(u, v): + edata = edge_data(G, u, v) + forward = True + else: + edata = edge_data(G, v, u) + forward = False + rel = edata.get("relation", "") + conf = edata.get("confidence", "") + conf_str = f" [{conf}]" if conf else "" + if i == 0: + segments.append(G.nodes[u].get("label", u)) + if forward: + segments.append(f"--{rel}{conf_str}--> {G.nodes[v].get('label', v)}") + else: + segments.append(f"<--{rel}{conf_str}-- {G.nodes[v].get('label', v)}") + print(f"Shortest path ({hops} hops):\n " + " ".join(segments)) + from graphify import querylog + querylog.log_query( + kind="path", + question=f"{sys.argv[2]} -> {sys.argv[3]}", + corpus=str(gp), + nodes_returned=hops, + ) + + elif cmd == "explain": + if len(sys.argv) < 3: + print('Usage: graphify explain "" [--graph path]', file=sys.stderr) + sys.exit(1) + from graphify.serve import _find_node + from networkx.readwrite import json_graph + + label = sys.argv[2] + graph_path = _default_graph_path() + args = sys.argv[3:] + for i, a in enumerate(args): + if a == "--graph" and i + 1 < len(args): + graph_path = args[i + 1] + gp = Path(graph_path).resolve() + if not gp.exists(): + print(f"error: graph file not found: {gp}", file=sys.stderr) + sys.exit(1) + _enforce_graph_size_cap_or_exit(gp) + _raw = json.loads(gp.read_text(encoding="utf-8")) + if "links" not in _raw and "edges" in _raw: + _raw = dict(_raw, links=_raw["edges"]) + # Force directed so the renderer can recover stored caller→callee direction. + _raw = {**_raw, "directed": True} + try: + G = json_graph.node_link_graph(_raw, edges="links") + except TypeError: + G = json_graph.node_link_graph(_raw) + matches = _find_node(G, label) + if not matches: + print(f"No node matching '{label}' found.") + sys.exit(0) + nid = matches[0] + d = G.nodes[nid] + print(f"Node: {d.get('label', nid)}") + print(f" ID: {nid}") + print( + f" Source: {d.get('source_file', '')} {d.get('source_location', '')}".rstrip() + ) + print(f" Type: {d.get('file_type', '')}") + print(f" Community: {d.get('community_name') or d.get('community', '')}") + # Work-memory overlay: a derived experiential hint from `graphify reflect`, + # merged in display-only from the .graphify_learning.json sidecar next to + # graph.json. No line when the node has no overlay entry. + try: + from graphify.reflect import load_learning_overlay as _llo + from graphify.security import sanitize_label as _sl + _overlay = _llo(gp) + _entry = _overlay.get(str(nid)) + if _entry: + _status = _sl(str(_entry.get("status", ""))) + if _status == "contested": + _line = (f" Lesson: contested (useful {_entry.get('uses', 0)} / " + f"dead-end {_entry.get('neg', 0)})") + elif _status == "preferred": + _line = (f" Lesson: preferred source (start here) — " + f"{_entry.get('uses', 0)} useful, score={_entry.get('score', 0)}") + else: + _line = (f" Lesson: {_status or 'tentative'} — " + f"{_entry.get('uses', 0)} useful, score={_entry.get('score', 0)}") + if _entry.get("stale"): + _line += " [code changed since — re-verify]" + print(_line) + except Exception: + pass + print(f" Degree: {G.degree(nid)}") + from graphify.build import edge_data + connections: list[tuple[str, str, dict]] = [] # (direction, neighbor_id, edge_data) + for nb in G.successors(nid): + connections.append(("out", nb, edge_data(G, nid, nb))) + for nb in G.predecessors(nid): + connections.append(("in", nb, edge_data(G, nb, nid))) + if connections: + print(f"\nConnections ({len(connections)}):") + connections.sort(key=lambda c: G.degree(c[1]), reverse=True) + for direction, nb, edata in connections[:20]: + rel = edata.get("relation", "") + conf = edata.get("confidence", "") + arrow = "-->" if direction == "out" else "<--" + print(f" {arrow} {G.nodes[nb].get('label', nb)} [{rel}] [{conf}]") + if len(connections) > 20: + print(f" ... and {len(connections) - 20} more") + from graphify import querylog + querylog.log_query( + kind="explain", + question=sys.argv[2], + corpus=str(gp), + nodes_returned=len(connections), + ) + + elif cmd == "diagnose": + subcmd = sys.argv[2] if len(sys.argv) > 2 else "" + if subcmd != "multigraph": + print( + "Usage: graphify diagnose multigraph " + "[--graph path] [--json] [--max-examples N] " + "[--directed] [--undirected] [--extract-path path]", + file=sys.stderr, + ) + sys.exit(1) + + graph_path = Path(_default_graph_path()) + max_examples = 5 + directed: bool | None = None + direction_flag: str | None = None + json_output = False + extract_path: Path | None = None + + i = 3 + while i < len(sys.argv): + arg = sys.argv[i] + if arg == "--graph": + i += 1 + if i >= len(sys.argv): + print("error: --graph requires a path", file=sys.stderr) + sys.exit(1) + graph_path = Path(sys.argv[i]) + elif arg == "--json": + json_output = True + elif arg == "--max-examples": + i += 1 + if i >= len(sys.argv): + print("error: --max-examples requires an integer", file=sys.stderr) + sys.exit(1) + try: + max_examples = int(sys.argv[i]) + except ValueError: + print("error: --max-examples requires an integer", file=sys.stderr) + sys.exit(1) + if max_examples < 0: + print("error: --max-examples must be >= 0", file=sys.stderr) + sys.exit(1) + elif arg == "--directed": + if direction_flag == "undirected": + print( + "error: --directed and --undirected are mutually exclusive", + file=sys.stderr, + ) + sys.exit(1) + direction_flag = "directed" + directed = True + elif arg == "--undirected": + if direction_flag == "directed": + print( + "error: --directed and --undirected are mutually exclusive", + file=sys.stderr, + ) + sys.exit(1) + direction_flag = "undirected" + directed = False + elif arg == "--extract-path": + i += 1 + if i >= len(sys.argv): + print("error: --extract-path requires a path", file=sys.stderr) + sys.exit(1) + extract_path = Path(sys.argv[i]) + else: + print(f"error: unknown diagnose option {arg}", file=sys.stderr) + sys.exit(1) + i += 1 + + from graphify.diagnostics import ( + diagnose_file, + format_diagnostic_json, + format_diagnostic_report, + ) + + try: + summary = diagnose_file( + graph_path, + directed=directed, + root=Path(".").resolve(), + max_examples=max_examples, + extract_path=extract_path, + ) + except Exception as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(1) + + if json_output: + print(json.dumps(format_diagnostic_json(summary), indent=2)) + else: + print(format_diagnostic_report(summary)) + + elif cmd == "add": + if len(sys.argv) < 3: + print( + "Usage: graphify add [--author Name] [--contributor Name] [--dir ./raw]", + file=sys.stderr, + ) + sys.exit(1) + from graphify.ingest import ingest as _ingest + + url = sys.argv[2] + author: str | None = None + contributor: str | None = None + target_dir = Path("raw") + args = sys.argv[3:] + i = 0 + while i < len(args): + if args[i] == "--author" and i + 1 < len(args): + author = args[i + 1] + i += 2 + elif args[i] == "--contributor" and i + 1 < len(args): + contributor = args[i + 1] + i += 2 + elif args[i] == "--dir" and i + 1 < len(args): + target_dir = Path(args[i + 1]) + i += 2 + else: + i += 1 + try: + saved = _ingest(url, target_dir, author=author, contributor=contributor) + print(f"Saved to {saved}") + print("Run /graphify --update in your AI assistant to update the graph.") + except Exception as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(1) + + elif cmd == "watch": + watch_path = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(".") + if not watch_path.exists(): + print(f"error: path not found: {watch_path}", file=sys.stderr) + sys.exit(1) + from graphify.watch import watch as _watch + + try: + _watch(watch_path) + except ImportError as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(1) + + elif cmd in ("cluster-only", "label"): + # `label` is `cluster-only` that always (re)generates community names with + # the configured backend, even when a .graphify_labels.json already exists. + force_relabel = cmd == "label" + # Mirror the tree/export arg-parsing pattern: walk argv so flags and + # the optional positional path can appear in any order (#724). + no_viz = "--no-viz" in sys.argv + no_label = "--no-label" in sys.argv + missing_only = "--missing-only" in sys.argv + co_timing = "--timing" in sys.argv + _backend_arg = next((a for a in sys.argv if a.startswith("--backend=")), None) + label_backend = _backend_arg.split("=", 1)[1] if _backend_arg else None + _model_arg = next((a for a in sys.argv if a.startswith("--model=")), None) + label_model = _model_arg.split("=", 1)[1] if _model_arg else None + _min_cs_arg = next((a for a in sys.argv if a.startswith("--min-community-size=")), None) + min_community_size = int(_min_cs_arg.split("=")[1]) if _min_cs_arg else 3 + args = sys.argv[2:] + watch_path: Path | None = None + graph_override: Path | None = None + co_resolution: float = 1.0 + co_exclude_hubs: float | None = None + label_max_concurrency: int = 4 + label_batch_size: int = 100 + i_arg = 0 + while i_arg < len(args): + a = args[i_arg] + if a == "--graph" and i_arg + 1 < len(args): + graph_override = Path(args[i_arg + 1]); i_arg += 2 + elif a == "--backend" and i_arg + 1 < len(args): + label_backend = args[i_arg + 1]; i_arg += 2 + elif a.startswith("--backend="): + label_backend = a.split("=", 1)[1]; i_arg += 1 + elif a == "--model" and i_arg + 1 < len(args): + label_model = args[i_arg + 1]; i_arg += 2 + elif a.startswith("--model="): + label_model = a.split("=", 1)[1]; i_arg += 1 + elif a == "--resolution" and i_arg + 1 < len(args): + co_resolution = float(args[i_arg + 1]); i_arg += 2 + elif a.startswith("--resolution="): + co_resolution = float(a.split("=", 1)[1]); i_arg += 1 + elif a == "--exclude-hubs" and i_arg + 1 < len(args): + co_exclude_hubs = float(args[i_arg + 1]); i_arg += 2 + elif a.startswith("--exclude-hubs="): + co_exclude_hubs = float(a.split("=", 1)[1]); i_arg += 1 + elif a == "--max-concurrency" and i_arg + 1 < len(args): + label_max_concurrency = int(args[i_arg + 1]); i_arg += 2 + elif a.startswith("--max-concurrency="): + label_max_concurrency = int(a.split("=", 1)[1]); i_arg += 1 + elif a == "--batch-size" and i_arg + 1 < len(args): + label_batch_size = int(args[i_arg + 1]); i_arg += 2 + elif a.startswith("--batch-size="): + label_batch_size = int(a.split("=", 1)[1]); i_arg += 1 + elif a in ("--no-viz", "--missing-only") or a.startswith("--min-community-size="): + i_arg += 1 + elif a.startswith("--"): + i_arg += 1 + elif watch_path is None: + watch_path = Path(a); i_arg += 1 + else: + i_arg += 1 + if watch_path is None: + watch_path = Path(".") + graph_json = graph_override if graph_override is not None else watch_path / _GRAPHIFY_OUT / "graph.json" + if not graph_json.exists(): + print( + f"error: no graph found at {graph_json} — run /graphify first", + file=sys.stderr, + ) + sys.exit(1) + from networkx.readwrite import json_graph as _jg + from graphify.build import build_from_json + from graphify.cluster import cluster, score_all, remap_communities_to_previous + from graphify.analyze import ( + god_nodes, + surprising_connections, + suggest_questions, + ) + from graphify.report import generate + from graphify.export import to_json, to_html + + stages = _StageTimer(co_timing) + print("Loading existing graph...") + # Solution 3 (#1019): don't hard-exit on an oversized graph.json here. + # Core outputs (graph.json + GRAPH_REPORT.md) still get written; the + # graph.html render below falls back to the community-aggregation view + # (node_limit=5000) when over the cap. + from graphify.security import check_graph_file_size_cap as _check_cap + _over_cap = False + try: + _check_cap(graph_json) + except ValueError: + _over_cap = True + try: + _over_cap_bytes = graph_json.stat().st_size + except OSError: + _over_cap_bytes = -1 + print( + f"warning: graph.json exceeds cap ({_over_cap_bytes} bytes); " + f"falling back to community-aggregation view (node_limit=5000)", + file=sys.stderr, + ) + _raw = json.loads(graph_json.read_text(encoding="utf-8")) + _directed = bool(_raw.get("directed", False)) + G = build_from_json(_raw, directed=_directed) + print(f"Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges") + stages.mark("load") + print("Re-clustering...") + communities = cluster(G, resolution=co_resolution, exclude_hubs_percentile=co_exclude_hubs) + # Mirror the watch/update path (#822): map new cids to prior ones by + # node-overlap so the existing .graphify_labels.json keeps attaching + # to the same conceptual community after re-clustering. Without this, + # labels follow raw cid index and become misaligned whenever the + # graph has changed between labeling and cluster-only (#1027). + previous_node_community = { + n["id"]: n["community"] + for n in _raw.get("nodes", []) + if n.get("community") is not None and n.get("id") is not None + } + if previous_node_community: + communities = remap_communities_to_previous(communities, previous_node_community) + stages.mark("cluster") + cohesion = score_all(G, communities) + gods = god_nodes(G) + surprises = surprising_connections(G, communities) + stages.mark("analyze") + # Where outputs (GRAPH_REPORT.md, re-clustered graph.json, labels, + # analysis, html) land. When `--graph` points at a graph INSIDE a + # graphify-out/ dir (another project/tenant's output), write beside it, + # not into a stray graphify-out/ in the CWD (#1747). But when `--graph` + # points at an arbitrary path — e.g. a `backup/graph.json` archived + # before re-clustering (#934) — fall back to the CWD's graphify-out/, + # which is the restore-into-place workflow that test pins. The default + # (no --graph) case already has graph_json under watch_path/graphify-out. + _out_name = Path(_GRAPHIFY_OUT).name + if graph_override is not None and graph_json.parent.name == _out_name: + out = graph_json.parent + else: + out = watch_path / _GRAPHIFY_OUT + out.mkdir(parents=True, exist_ok=True) + labels_path = out / ".graphify_labels.json" + existing_labels: dict[int, str] = {} + if labels_path.exists(): + try: + existing_labels = { + int(k): v + for k, v in json.loads(labels_path.read_text(encoding="utf-8")).items() + if isinstance(v, str) + } + except Exception: + existing_labels = {} + # Accumulate token usage from the labeling LLM calls so cluster-only mode + # reports real cost instead of a hardcoded zero (#1694). Stays {0, 0} on + # the reuse / no-label paths, which make no LLM calls. + label_token_usage = {"input": 0, "output": 0} + if labels_path.exists() and not force_relabel: + # Reuse saved labels, but don't blindly trust them: the graph may have + # been re-scoped/re-clustered since labeling, in which case a cid now + # covers a DIFFERENT community and its old (LLM) name is wrong (#label-stale). + # Validate each community against the membership signature saved beside the + # labels; any community that changed (or has no saved label) is renamed by + # its current hub — deterministic and correct-by-construction — and the user + # is told to `graphify label` for fresh LLM names. Unchanged communities keep + # their saved label. When no signature sidecar exists (labels predate this), + # fall back to hub-filling only the communities missing a label. + from graphify.cluster import community_member_sigs, label_communities_by_hub + sig_path = labels_path.parent / (labels_path.name + ".sig") + saved_sigs: dict[int, str] = {} + if sig_path.exists(): + try: + saved_sigs = { + int(k): v for k, v in + json.loads(sig_path.read_text(encoding="utf-8")).items() + if isinstance(v, str) + } + except Exception: + saved_sigs = {} + cur_sigs = community_member_sigs(communities) + count_mismatch = len(existing_labels) != len(communities) + labels = {} + hub_labels: dict[int, str] | None = None + changed = 0 + for cid in communities: + have_label = cid in existing_labels + if saved_sigs: + # Precise: the membership signature tells us if this exact + # community changed since it was labeled. + fresh = have_label and saved_sigs.get(cid) == cur_sigs.get(cid) + else: + # No signature sidecar (labels predate it). A differing community + # COUNT means the labels describe a different clustering, so a cid's + # old label can't be trusted; equal count is the best "same" signal. + fresh = have_label and not count_mismatch + if fresh: + labels[cid] = existing_labels[cid] + else: + if hub_labels is None: + hub_labels = label_communities_by_hub(G, communities) + labels[cid] = hub_labels[cid] + if have_label: + changed += 1 + if changed: + print( + f"[graphify] community set changed since labeling " + f"({len(existing_labels)} saved labels, {len(communities)} communities now; " + f"renamed {changed} community(ies) by their hub). " + f"Run `graphify label` to refresh names with the LLM.", + file=sys.stderr, + ) + elif no_label and not force_relabel: + labels = {cid: f"Community {cid}" for cid in communities} + else: + # No labels file yet (or `graphify label` forced a refresh). When run + # standalone there is no orchestrating agent to do skill.md Step 5, so + # auto-name communities rather than leave "Community N" (#1097). + from graphify.cluster import label_communities_by_hub + from graphify.llm import generate_community_labels + print("Labeling communities...") + # Deterministic, LLM-free base labels: name each community after its + # highest-degree hub, so the report is readable even with no backend + # (previously bare "Community N"). A configured LLM backend overrides these + # with richer names below; its no-backend placeholder fallback does NOT. + hub_labels = label_communities_by_hub(G, communities) + label_communities_input = communities + labels = dict(hub_labels) + if missing_only: + labels = { + cid: existing_labels.get(cid, hub_labels[cid]) + for cid in communities + } + label_communities_input = { + cid: members + for cid, members in communities.items() + if cid not in existing_labels or existing_labels.get(cid) == f"Community {cid}" + } + generated_labels, _ = generate_community_labels( + G, label_communities_input, backend=label_backend, model=label_model, gods=gods, + max_concurrency=label_max_concurrency, batch_size=label_batch_size, + usage_out=label_token_usage, + ) + # Only let the LLM OVERRIDE where it produced a real name — its no-backend + # fallback returns "Community {cid}" placeholders, which must not clobber + # the deterministic hub labels. + labels.update({ + cid: v for cid, v in generated_labels.items() + if v and v != f"Community {cid}" + }) + stages.mark("label") + questions = suggest_questions(G, communities, labels) + tokens = label_token_usage + from graphify.export import _git_head as _gh + _commit = _gh() + from graphify.report import load_learning_for_report as _llfr + report = generate(G, communities, cohesion, labels, gods, surprises, + {"warning": "cluster-only mode — file stats not available"}, + tokens, str(watch_path), suggested_questions=questions, + min_community_size=min_community_size, built_at_commit=_commit, + learning=_llfr(out / "graph.json")) + (out / "GRAPH_REPORT.md").write_text(report, encoding="utf-8") + stages.mark("report") + from graphify.export import backup_if_protected as _backup + _backup(out) + analysis = { + "communities": {str(k): v for k, v in communities.items()}, + "cohesion": {str(k): v for k, v in cohesion.items()}, + "gods": gods, + "surprises": surprises, + "questions": questions, + } + (out / ".graphify_analysis.json").write_text( + json.dumps(analysis, indent=2, ensure_ascii=False), + encoding="utf-8", + ) + to_json(G, communities, str(out / "graph.json"), community_labels=labels) + labels_path.write_text(json.dumps({str(k): v for k, v in labels.items()}, ensure_ascii=False), encoding="utf-8") + # Membership signatures beside the labels so a later cluster-only can detect + # which communities changed and avoid reusing a stale label (see reuse above). + from graphify.cluster import community_member_sigs as _cms + (labels_path.parent / (labels_path.name + ".sig")).write_text( + json.dumps({str(k): v for k, v in _cms(communities).items()}), encoding="utf-8") + + # Mirror watch.py pattern: gate to_html so core outputs (graph.json + + # GRAPH_REPORT.md) always land. Honor --no-viz explicitly; otherwise + # fall back to ValueError handling so an oversized graph doesn't crash + # the CLI mid-write and leave a stale graph.html on disk. + html_target = out / "graph.html" + if no_viz: + if html_target.exists(): + html_target.unlink() + stages.mark("export"); stages.total() + print(f"Done - {len(communities)} communities. GRAPH_REPORT.md and graph.json updated (--no-viz; graph.html removed).") + else: + try: + # Over-cap fallback (#1019): force the community-aggregation + # path so an oversized graph still renders a usable graph.html. + _node_limit = 5000 if _over_cap else None + to_html(G, communities, str(html_target), community_labels=labels or None, + node_limit=_node_limit) + stages.mark("export"); stages.total() + print(f"Done - {len(communities)} communities. GRAPH_REPORT.md, graph.json and graph.html updated.") + except ValueError as viz_err: + if html_target.exists(): + html_target.unlink() + print(f"Skipped graph.html: {viz_err}") + stages.mark("export"); stages.total() + print(f"Done - {len(communities)} communities. GRAPH_REPORT.md and graph.json updated.") + + elif cmd == "update": + force = os.environ.get("GRAPHIFY_FORCE", "").lower() in ("1", "true", "yes") + no_cluster = False + args = sys.argv[2:] + watch_arg: str | None = None + for a in args: + if a == "--force": + force = True + continue + if a == "--no-cluster": + no_cluster = True + continue + if a.startswith("-"): + print(f"error: unknown update option: {a}", file=sys.stderr) + sys.exit(2) + if watch_arg is not None: + print("error: update accepts at most one path argument", file=sys.stderr) + sys.exit(2) + watch_arg = a + + if watch_arg is not None: + watch_path = Path(watch_arg) + else: + # Try to recover the scan root saved by the last full build + saved = Path(_GRAPHIFY_OUT) / ".graphify_root" + if saved.exists(): + watch_path = Path(saved.read_text(encoding="utf-8").strip()) + else: + watch_path = Path(".") + if not watch_path.exists(): + print(f"error: path not found: {watch_path}", file=sys.stderr) + sys.exit(1) + from graphify.watch import _rebuild_code + + print(f"Re-extracting code files in {watch_path} (no LLM needed)...") + # Interactive CLI: block on the per-repo lock rather than skip, so the + # user sees their explicit `graphify update` complete instead of + # exiting silently when a hook-driven rebuild happens to be running. + ok = _rebuild_code(watch_path, force=force, no_cluster=no_cluster, block_on_lock=True) + if ok: + print("Code graph updated. For doc/paper/image changes run /graphify --update in your AI assistant.") + if not os.environ.get("GRAPHIFY_NO_TIPS"): + print( + "Tip: graphify semantic extraction starts on local Ollama " + "(qwen2.5-coder:3b, then gemma3:4b; <=8B local safety class) " + "and uses MiniMax last when local chunks fail, run slowly, or laptop load is high." + ) + else: + print( + "Nothing to update or rebuild failed — check output above.", + file=sys.stderr, + ) + sys.exit(1) + + elif cmd == "hook-check": + # Codex Desktop rejects hookSpecificOutput.additionalContext on PreToolUse. + # Keep this as a cross-platform no-op so installed hooks never break Bash + # tool calls. Graph guidance reaches the agent via AGENTS.md / skill instead. + sys.exit(0) + elif cmd == "hook-guard": + # Shell-agnostic Claude/Codebuddy PreToolUse guard (#522). Replaces the old + # inline-bash hooks that failed on Windows. Prints an additionalContext nudge + # toward graphify when a graph exists; always exits 0 (never blocks a tool). + _run_hook_guard(sys.argv[2] if len(sys.argv) > 2 else "") + sys.exit(0) + elif cmd == "check-update": + if len(sys.argv) < 3: + print("Usage: graphify check-update ", file=sys.stderr) + sys.exit(1) + from graphify.watch import check_update + + check_update(Path(sys.argv[2]).resolve()) + sys.exit(0) + elif cmd == "tree": + # Emit a D3 v7 collapsible-tree HTML view of graph.json: + # expand-all / collapse-all / reset-view buttons, multi-line + # wrapText labels with separately-coloured name + count, + # depth-based palette, click-to-toggle subtree, hover inspector + # showing top-K outbound edges per symbol. + from typing import Optional as _Opt + from graphify.tree_html import write_tree_html, DEFAULT_MAX_CHILDREN + graph_path = Path(_GRAPHIFY_OUT) / "graph.json" + output_path: "_Opt[Path]" = None + root: "_Opt[str]" = None + max_children = DEFAULT_MAX_CHILDREN + top_k_edges = 0 + project_label: "_Opt[str]" = None + args = sys.argv[2:] + i_arg = 0 + while i_arg < len(args): + a = args[i_arg] + if a == "--graph" and i_arg + 1 < len(args): + graph_path = Path(args[i_arg + 1]); i_arg += 2 + elif a == "--output" and i_arg + 1 < len(args): + output_path = Path(args[i_arg + 1]); i_arg += 2 + elif a == "--root" and i_arg + 1 < len(args): + root = args[i_arg + 1]; i_arg += 2 + elif a == "--max-children" and i_arg + 1 < len(args): + max_children = int(args[i_arg + 1]); i_arg += 2 + elif a == "--top-k-edges" and i_arg + 1 < len(args): + top_k_edges = int(args[i_arg + 1]); i_arg += 2 + elif a == "--label" and i_arg + 1 < len(args): + project_label = args[i_arg + 1]; i_arg += 2 + elif a in ("-h", "--help"): + print("Usage: graphify tree [--graph PATH] [--output HTML]") + print(" --graph PATH path to graph.json (default graphify-out/graph.json)") + print(" --output HTML output path (default graphify-out/GRAPH_TREE.html)") + print(" --root PATH filesystem root (default: longest common dir of all source_files)") + print(" --max-children N cap visible children per node (default 200)") + print(" --top-k-edges N pre-compute top-K outbound edges per symbol (default 12)") + print(" --label NAME project label shown in the page header") + return + else: + i_arg += 1 + if not graph_path.is_file(): + print(f"error: graph.json not found at {graph_path}", file=sys.stderr) + sys.exit(1) + _enforce_graph_size_cap_or_exit(graph_path) + if output_path is None: + output_path = graph_path.parent / "GRAPH_TREE.html" + out = write_tree_html( + graph_path=graph_path, output_path=output_path, + root=root, max_children=max_children, + top_k_edges=top_k_edges, project_label=project_label, + ) + size_kb = out.stat().st_size / 1024 + print(f"wrote {out} ({size_kb:.1f} KB)") + print(f"open with: xdg-open {out} (or file://{out.resolve()})") + sys.exit(0) + + elif cmd == "merge-driver": + # git merge driver for graph.json — takes (base, current, other) and writes + # the union of current+other nodes/edges back to current. Exits 1 on + # corrupt input so git surfaces the conflict instead of silently + # accepting a poisoned merge (see F-005). + # Usage: graphify merge-driver %O %A %B (set in .git/config merge driver) + if len(sys.argv) < 5: + print("Usage: graphify merge-driver ", file=sys.stderr) + sys.exit(1) + _base_path, _current_path, _other_path = sys.argv[2], sys.argv[3], sys.argv[4] + # Hard caps so a malicious or corrupted graph.json cannot exhaust memory + # at parse time. 50 MB / 100k nodes are well above any realistic graph + # (typical graphs are <5 MB / <50k nodes); anything larger should fail + # the merge so a human can investigate. + _MERGE_MAX_BYTES = 50 * 1024 * 1024 + _MERGE_MAX_NODES = 100_000 + import networkx as _nx + from networkx.readwrite import json_graph as _jg + def _load_graph(p: str): + path_obj = Path(p) + try: + size = path_obj.stat().st_size + except OSError as exc: + raise RuntimeError(f"cannot stat {p}: {exc}") from exc + if size > _MERGE_MAX_BYTES: + raise RuntimeError( + f"graph.json {p} is {size} bytes, exceeds {_MERGE_MAX_BYTES}-byte cap" + ) + data = json.loads(path_obj.read_text(encoding="utf-8")) + try: + return _jg.node_link_graph(data, edges="links"), data + except TypeError: + return _jg.node_link_graph(data), data + try: + G_cur, _ = _load_graph(_current_path) + G_oth, _ = _load_graph(_other_path) + except Exception as exc: + print(f"[graphify merge-driver] error loading graphs: {exc}", file=sys.stderr) + sys.exit(1) # surface the conflict so git doesn't accept a corrupt merge + merged = _nx.compose(G_cur, G_oth) + if merged.number_of_nodes() > _MERGE_MAX_NODES: + print( + f"[graphify merge-driver] merged graph has {merged.number_of_nodes()} nodes, " + f"exceeds {_MERGE_MAX_NODES}-node cap; aborting merge.", + file=sys.stderr, + ) + sys.exit(1) + try: + out_data = _jg.node_link_data(merged, edges="links") + except TypeError: + out_data = _jg.node_link_data(merged) + Path(_current_path).write_text(json.dumps(out_data, indent=2), encoding="utf-8") + sys.exit(0) + + elif cmd == "merge-graphs": + # graphify merge-graphs graph1.json graph2.json ... --out merged.json + args = sys.argv[2:] + graph_paths: list[Path] = [] + out_path = Path(_GRAPHIFY_OUT) / "merged-graph.json" + i = 0 + while i < len(args): + if args[i] == "--out" and i + 1 < len(args): + out_path = Path(args[i + 1]) + i += 2 + else: + graph_paths.append(Path(args[i])) + i += 1 + if len(graph_paths) < 2: + print( + "Usage: graphify merge-graphs [...] [--out merged.json]", + file=sys.stderr, + ) + sys.exit(1) + import networkx as _nx + from networkx.readwrite import json_graph as _jg + from graphify.build import prefix_graph_for_global as _prefix, distinct_repo_tags as _repo_tags + graphs = [] + for gp in graph_paths: + if not gp.exists(): + print(f"error: not found: {gp}", file=sys.stderr) + sys.exit(1) + _enforce_graph_size_cap_or_exit(gp) + data = json.loads(gp.read_text(encoding="utf-8")) + # Normalize edges/links key before loading — graphify writes "links" + # via node_link_data but older runs may have used "edges" (#738). + if "links" not in data and "edges" in data: + data = dict(data, links=data["edges"]) + try: + G = _jg.node_link_graph(data, edges="links") + except TypeError: + G = _jg.node_link_graph(data) + graphs.append(G) + # nx.compose requires all graphs to be the same type. When input graphs + # come from different sources (e.g. an AST-only run vs a full LLM run) one + # may be a MultiGraph and another a Graph. Normalise everything to Graph + # (the graphify default) by converting MultiGraphs with nx.Graph(). + def _to_simple(g: "_nx.Graph") -> "_nx.Graph": + # nx.compose requires every graph to be the same type. Inputs may + # disagree on BOTH axes — directed vs undirected, and multi vs simple + # — because per-repo graph.json files are written by different extract + # paths at different times. Normalise everything to a plain undirected + # Graph (the merged cross-repo view is undirected anyway), which covers + # DiGraph / MultiGraph / MultiDiGraph. Without this a directed input + # crashed compose with "All graphs must be directed or undirected" (#1606). + if type(g) is not _nx.Graph: + return _nx.Graph(g) + return g + # Unique repo tag per graph. The bare `graphify-out/..` dir name is not + # unique across inputs (src/graphify-out and frontend/src/graphify-out both + # → "src"), which collides same-stem node ids and silently merges unrelated + # entities (#1729). distinct_repo_tags guarantees a distinct prefix per graph. + repo_tags = _repo_tags(graph_paths) + naive_tags = [gp.parent.parent.name for gp in graph_paths] + if len(set(naive_tags)) != len(naive_tags): + print(f" note: repo dir names collide; using distinct tags: {', '.join(repo_tags)}") + merged = _nx.Graph() + for G, repo_tag in zip(graphs, repo_tags): + prefixed = _to_simple(_prefix(G, repo_tag)) + merged = _nx.compose(merged, prefixed) + try: + out_data = _jg.node_link_data(merged, edges="links") + except TypeError: + out_data = _jg.node_link_data(merged) + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(json.dumps(out_data, indent=2), encoding="utf-8") + print(f"Merged {len(graphs)} graphs -> {merged.number_of_nodes()} nodes, {merged.number_of_edges()} edges") + print(f"Written to: {out_path}") + + elif cmd == "clone": + if len(sys.argv) < 3: + print( + "Usage: graphify clone [--branch ] [--out ]", + file=sys.stderr, + ) + sys.exit(1) + url = sys.argv[2] + branch: str | None = None + out_dir: Path | None = None + args = sys.argv[3:] + i = 0 + while i < len(args): + if args[i] == "--branch" and i + 1 < len(args): + branch = args[i + 1] + i += 2 + elif args[i] == "--out" and i + 1 < len(args): + out_dir = Path(args[i + 1]) + i += 2 + else: + i += 1 + local_path = _clone_repo(url, branch=branch, out_dir=out_dir) + print(local_path) + + elif cmd == "export": + subcmd = sys.argv[2] if len(sys.argv) > 2 else "" + if subcmd not in ("html", "callflow-html", "obsidian", "wiki", "svg", "graphml", "neo4j", "falkordb"): + print("Usage: graphify export ", file=sys.stderr) + print(" html [--graph PATH] [--labels PATH] [--node-limit N] [--no-viz]", file=sys.stderr) + print(" callflow-html [GRAPH|DIR] [--graph PATH] [--labels PATH] [--report PATH] [--sections PATH] [--output HTML]", file=sys.stderr) + print(" [--lang auto|zh-CN|en] [--max-sections N] [--diagram-scale N]", file=sys.stderr) + print(" obsidian [--graph PATH] [--labels PATH] [--dir PATH]", file=sys.stderr) + print(" wiki [--graph PATH] [--labels PATH]", file=sys.stderr) + print(" svg [--graph PATH] [--labels PATH]", file=sys.stderr) + print(" graphml [--graph PATH]", file=sys.stderr) + print(" neo4j [--graph PATH] [--push URI] [--user U] [--password P]", file=sys.stderr) + print(" (or set NEO4J_PASSWORD instead of --password to keep it off argv)", file=sys.stderr) + print(" falkordb [--graph PATH] [--push URI] [--user U] [--password P]", file=sys.stderr) + print(" (or set FALKORDB_PASSWORD instead of --password to keep it off argv)", file=sys.stderr) + sys.exit(1) + + # Parse shared args + args = sys.argv[3:] + graph_path = Path(_GRAPHIFY_OUT) / "graph.json" + graph_path_explicit = False + labels_path = Path(_GRAPHIFY_OUT) / ".graphify_labels.json" + labels_path_explicit = False + report_path = Path(_GRAPHIFY_OUT) / "GRAPH_REPORT.md" + report_path_explicit = False + sections_path: Path | None = None + callflow_output: Path | None = None + callflow_lang = "auto" + callflow_max_sections = 15 + callflow_diagram_scale = 1.0 + callflow_max_diagram_nodes = 18 + callflow_max_diagram_edges = 24 + analysis_path = Path(_GRAPHIFY_OUT) / ".graphify_analysis.json" + node_limit = 5000 + no_viz = False + obsidian_dir = Path(_GRAPHIFY_OUT) / "obsidian" + # Shared push-connection settings for the graph-database sinks (neo4j, + # falkordb), parsed from the generic --push/--user/--password flags below. + push_uri: str | None = None + push_user = "neo4j" # Neo4j default user; FalkorDB auth is optional and ignores it + # F-031: prefer an env var so the password never appears on argv (visible + # in `ps` output / shell history). The explicit --password flag still + # overrides it. Each sink reads its own var: FALKORDB_PASSWORD for falkordb, + # NEO4J_PASSWORD otherwise. + push_password: str | None = ( + os.environ.get("FALKORDB_PASSWORD") if subcmd == "falkordb" + else os.environ.get("NEO4J_PASSWORD") + ) or None + i = 0 + while i < len(args): + a = args[i] + if a == "--graph" and i + 1 < len(args): + graph_path = Path(args[i + 1]) + graph_path_explicit = True + i += 2 + elif a == "--labels" and i + 1 < len(args): + labels_path = Path(args[i + 1]) + labels_path_explicit = True + i += 2 + elif a == "--report" and i + 1 < len(args): + report_path = Path(args[i + 1]) + report_path_explicit = True + i += 2 + elif a == "--sections" and i + 1 < len(args): + sections_path = Path(args[i + 1]); i += 2 + elif a == "--output" and i + 1 < len(args): + callflow_output = Path(args[i + 1]).expanduser() + if not callflow_output.is_absolute(): + callflow_output = Path.cwd() / callflow_output + i += 2 + elif a == "--lang" and i + 1 < len(args): + callflow_lang = args[i + 1]; i += 2 + elif a == "--max-sections" and i + 1 < len(args): + callflow_max_sections = int(args[i + 1]); i += 2 + elif a == "--diagram-scale" and i + 1 < len(args): + callflow_diagram_scale = float(args[i + 1]); i += 2 + elif a == "--max-diagram-nodes" and i + 1 < len(args): + callflow_max_diagram_nodes = int(args[i + 1]); i += 2 + elif a == "--max-diagram-edges" and i + 1 < len(args): + callflow_max_diagram_edges = int(args[i + 1]); i += 2 + elif a in ("-h", "--help") and subcmd == "callflow-html": + print("Usage: graphify export callflow-html [GRAPH|DIR] [--graph PATH] [--labels PATH]") + print(" --report PATH path to GRAPH_REPORT.md") + print(" --sections PATH JSON section definitions") + print(" --output HTML output path (default graphify-out/-callflow.html)") + print(" --lang LANG auto, zh-CN, en, etc. (default auto)") + print(" --max-sections N maximum auto-derived sections (default 15)") + print(" --diagram-scale N Mermaid diagram scale (default 1.0)") + print(" --max-diagram-nodes N representative nodes per section (default 18)") + print(" --max-diagram-edges N representative edges per section (default 24)") + sys.exit(0) + elif a == "--node-limit" and i + 1 < len(args): + node_limit = int(args[i + 1]); i += 2 + elif a == "--no-viz": + no_viz = True; i += 1 + elif a == "--dir" and i + 1 < len(args): + obsidian_dir = Path(args[i + 1]); i += 2 + elif a == "--push" and i + 1 < len(args): + push_uri = args[i + 1]; i += 2 + elif a == "--user" and i + 1 < len(args): + push_user = args[i + 1]; i += 2 + elif a == "--password" and i + 1 < len(args): + push_password = args[i + 1]; i += 2 + elif subcmd == "callflow-html" and not a.startswith("-") and not graph_path_explicit: + candidate = Path(a) + if candidate.name == "graph.json" or candidate.suffix.lower() == ".json": + graph_path = candidate + elif (candidate / "graph.json").exists(): + graph_path = candidate / "graph.json" + else: + graph_path = candidate / _GRAPHIFY_OUT / "graph.json" + graph_path_explicit = True + i += 1 + else: + i += 1 + + graph_path = graph_path.expanduser() + if graph_path_explicit: + graph_out_dir = graph_path.parent + if not labels_path_explicit: + labels_path = graph_out_dir / ".graphify_labels.json" + if not report_path_explicit: + report_path = graph_out_dir / "GRAPH_REPORT.md" + labels_path = labels_path.expanduser() + report_path = report_path.expanduser() + + if not graph_path.exists(): + print(f"error: graph not found: {graph_path}. Run /graphify first.", file=sys.stderr) + sys.exit(1) + + if subcmd == "callflow-html": + from graphify.callflow_html import write_callflow_html as _write_callflow_html + out = _write_callflow_html( + graph=graph_path, + report=report_path, + labels=labels_path, + sections=sections_path, + output=callflow_output, + lang=callflow_lang, + max_sections=callflow_max_sections, + diagram_scale=callflow_diagram_scale, + max_diagram_nodes=callflow_max_diagram_nodes, + max_diagram_edges=callflow_max_diagram_edges, + verbose=True, + ) + print(f"callflow HTML written - open in any browser: {out}") + sys.exit(0) + + from networkx.readwrite import json_graph as _jg + from graphify.build import build_from_json as _bfj + from graphify.security import check_graph_file_size_cap as _check_cap + + # Solution 3 (#1019): for the HTML view, an oversized graph.json should + # not be a hard error. Detect the over-cap condition here and fall back + # to the community-aggregation view (node_limit=5000) below instead of + # exiting 1. All other subcommands keep the hard cap. + _over_cap = False + try: + _check_cap(graph_path) + except ValueError as _cap_err: + if subcmd == "html": + _over_cap = True + try: + _over_cap_bytes = graph_path.stat().st_size + except OSError: + _over_cap_bytes = -1 + print( + f"warning: graph.json exceeds cap ({_over_cap_bytes} bytes); " + f"falling back to community-aggregation view (node_limit=5000)", + file=sys.stderr, + ) + else: + print(f"error: {_cap_err}", file=sys.stderr) + sys.exit(1) + _raw = json.loads(graph_path.read_text(encoding="utf-8")) + if "links" not in _raw and "edges" in _raw: + _raw = dict(_raw, links=_raw["edges"]) + try: + G = _jg.node_link_graph(_raw, edges="links") + except TypeError: + G = _jg.node_link_graph(_raw) + + # Load optional analysis/labels + communities: dict[int, list[str]] = {} + if analysis_path.exists(): + _an = json.loads(analysis_path.read_text(encoding="utf-8")) + communities = {int(k): v for k, v in _an.get("communities", {}).items()} + cohesion: dict[int, float] = {int(k): v for k, v in _an.get("cohesion", {}).items()} + gods_data = _an.get("gods", []) + else: + cohesion = {} + gods_data = [] + + # Fallback: graph.json carries the per-node community as a node attribute + # (`to_json` writes it on every node). The analysis sidecar is the + # canonical source — but the post-commit / watch rebuild path doesn't + # regenerate it, and `extract` may have its temp files cleaned up. When + # that happens, `graphify export html` previously bailed with + # "Single community - aggregated view not useful." even though the + # per-node attribute had the right data all along. Reconstruct from + # the graph itself so downstream subcommands (html, obsidian, wiki, + # svg, graphml, neo4j) don't silently produce a degraded artifact. + if not communities: + reconstructed: dict[int, list[str]] = {} + for node_id, data in G.nodes(data=True): + cid_raw = data.get("community") + if cid_raw is None: + continue + try: + cid = int(cid_raw) + except (TypeError, ValueError): + continue + reconstructed.setdefault(cid, []).append(str(node_id)) + if reconstructed: + communities = reconstructed + + labels: dict[int, str] = {} + if labels_path.exists(): + labels = {int(k): v for k, v in json.loads(labels_path.read_text(encoding="utf-8")).items()} + + out_dir = graph_path.parent + + if subcmd == "html": + from graphify.export import to_html as _to_html + if no_viz: + html_target = out_dir / "graph.html" + if html_target.exists(): + html_target.unlink() + print("--no-viz: skipped graph.html") + else: + # Over-cap fallback (#1019): force the community-aggregation + # path so the oversized graph still renders a usable artifact. + _effective_node_limit = 5000 if _over_cap else node_limit + _to_html(G, communities, str(out_dir / "graph.html"), + community_labels=labels or None, node_limit=_effective_node_limit) + if G.number_of_nodes() <= _effective_node_limit: + print(f"graph.html written - open in any browser, no server needed") + if _over_cap: + sys.exit(0) + + elif subcmd == "obsidian": + from graphify.export import to_obsidian as _to_obsidian, to_canvas as _to_canvas + n = _to_obsidian(G, communities, str(obsidian_dir), + community_labels=labels or None, cohesion=cohesion or None) + print(f"Obsidian vault: {n} notes in {obsidian_dir}/") + _to_canvas(G, communities, str(obsidian_dir / "graph.canvas"), + community_labels=labels or None) + print(f"Canvas: {obsidian_dir}/graph.canvas") + print(f"Open {obsidian_dir}/ as a vault in Obsidian.") + + elif subcmd == "wiki": + from graphify.wiki import to_wiki as _to_wiki + from graphify.analyze import god_nodes as _god_nodes + if not communities: + print( + "error: .graphify_analysis.json is missing or empty — refusing to export wiki to prevent data loss.\n" + "Run `graphify extract .` (or `graphify cluster-only .`) to regenerate community data first.", + file=sys.stderr, + ) + sys.exit(1) + if not gods_data: + gods_data = _god_nodes(G) + n = _to_wiki(G, communities, str(out_dir / "wiki"), + community_labels=labels or None, cohesion=cohesion or None, + god_nodes_data=gods_data) + print(f"Wiki: {n} articles written to {out_dir}/wiki/") + print(f" {out_dir}/wiki/index.md -> agent entry point") + + elif subcmd == "svg": + from graphify.export import to_svg as _to_svg + _to_svg(G, communities, str(out_dir / "graph.svg"), + community_labels=labels or None) + print(f"graph.svg written - embeds in Obsidian, Notion, GitHub READMEs") + + elif subcmd == "graphml": + from graphify.export import to_graphml as _to_graphml + _to_graphml(G, communities, str(out_dir / "graph.graphml")) + print(f"graph.graphml written - open in Gephi, yEd, or any GraphML tool") + + elif subcmd == "neo4j": + if push_uri: + from graphify.export import push_to_neo4j as _push + if push_password is None: + print("error: --password required for --push", file=sys.stderr) + sys.exit(1) + result = _push(G, uri=push_uri, user=push_user, + **{"password": push_password}, communities=communities) + print(f"Pushed to Neo4j: {result['nodes']} nodes, {result['edges']} edges") + else: + from graphify.export import to_cypher as _to_cypher + _to_cypher(G, str(out_dir / "cypher.txt")) + print(f"cypher.txt written - import with: cypher-shell < {out_dir}/cypher.txt") + + elif subcmd == "falkordb": + if push_uri: + from graphify.export import push_to_falkordb as _push + result = _push(G, uri=push_uri, user=push_user, + **{"password": push_password}, communities=communities) + print(f"Pushed to FalkorDB: {result['nodes']} nodes, {result['edges']} edges") + else: + from graphify.export import to_cypher as _to_cypher + _to_cypher(G, str(out_dir / "cypher.txt")) + print(f"cypher.txt written ({out_dir}/cypher.txt) - statements are OpenCypher. " + f"FalkorDB's GRAPH.QUERY runs one statement at a time (no bulk script " + f"import), so load a graph with: graphify export falkordb --push " + f"falkordb://localhost:6379") + + elif cmd == "benchmark": + from graphify.benchmark import run_benchmark, print_benchmark + + graph_path = sys.argv[2] if len(sys.argv) > 2 else _default_graph_path() + _enforce_graph_size_cap_or_exit(Path(graph_path)) + # Try to load corpus_words from detect output + corpus_words = None + detect_path = Path(".graphify_detect.json") + if detect_path.exists(): + try: + detect_data = json.loads(detect_path.read_text(encoding="utf-8")) + corpus_words = detect_data.get("total_words") + except Exception: + pass + result = run_benchmark(graph_path, corpus_words=corpus_words) + print_benchmark(result) + + elif cmd == "global": + subcmd = sys.argv[2] if len(sys.argv) > 2 else "" + from graphify.global_graph import ( + global_add as _global_add, + global_remove as _global_remove, + global_list as _global_list, + global_path as _global_path, + ) + if subcmd == "add": + # graphify global add [--as ] + args = sys.argv[3:] + source = None + tag = None + i = 0 + while i < len(args): + if args[i] == "--as" and i + 1 < len(args): + tag = args[i + 1]; i += 2 + elif not source: + source = Path(args[i]); i += 1 + else: + i += 1 + if not source: + print("Usage: graphify global add [--as ]", file=sys.stderr) + sys.exit(1) + tag = tag or source.parent.parent.name + try: + result = _global_add(source, tag) + if result["skipped"]: + print(f"'{tag}' unchanged since last add - global graph not modified.") + else: + print(f"Added '{tag}' to global graph: +{result['nodes_added']} nodes, " + f"-{result['nodes_removed']} pruned. Global: {_global_path()}") + except Exception as exc: + print(f"error: {exc}", file=sys.stderr); sys.exit(1) + elif subcmd == "remove": + tag = sys.argv[3] if len(sys.argv) > 3 else "" + if not tag: + print("Usage: graphify global remove ", file=sys.stderr); sys.exit(1) + try: + removed = _global_remove(tag) + print(f"Removed '{tag}' from global graph ({removed} nodes pruned).") + except KeyError as exc: + print(f"error: {exc}", file=sys.stderr); sys.exit(1) + elif subcmd == "list": + repos = _global_list() + if not repos: + print("Global graph is empty. Use 'graphify global add' to add a project.") + else: + print(f"Global graph: {_global_path()}") + for tag, info in repos.items(): + print(f" {tag}: {info.get('node_count', '?')} nodes, added {info.get('added_at', '?')[:10]}") + elif subcmd == "path": + print(_global_path()) + else: + print("Usage: graphify global [add|remove|list|path]", file=sys.stderr); sys.exit(1) + + elif cmd == "extract": + # Headless full-pipeline extraction for CI / scripts (#698). + # Runs detect -> AST extraction on code -> semantic LLM extraction on + # docs/papers/images -> merge -> build -> cluster -> write outputs. + if len(sys.argv) < 3: + print( + "Usage: graphify extract [--backend ollama|minimax|nim|gemini|kimi|claude|openai|deepseek] " + "[--model M] [--mode deep] [--out DIR] [--google-workspace] [--no-cluster] " + "[--max-workers N] [--token-budget N] [--max-concurrency N] " + "[--api-timeout S] [--postgres DSN] [--cargo] [--timing]", + file=sys.stderr, + ) + sys.exit(1) + + has_path = True + if sys.argv[2].startswith("-"): + has_path = False + target = Path(".").resolve() + else: + target = Path(sys.argv[2]).resolve() + if not target.exists(): + print(f"error: path not found: {target}", file=sys.stderr) + sys.exit(1) + + backend: str | None = None + model: str | None = None + extract_mode: str | None = None + out_dir: Path | None = None + cli_postgres_dsn: str | None = None + cli_cargo: bool = False + no_cluster = False + dedup_llm = False + google_workspace = False + global_merge = False + code_only = False + global_repo_tag: str | None = None + # Performance/tuning knobs (issue #792). None means "use library default". + cli_max_workers: int | None = None + cli_token_budget: int | None = None + cli_max_concurrency: int | None = None + cli_api_timeout: float | None = None + # Clustering tuning knobs + cli_resolution: float = 1.0 + cli_exclude_hubs: float | None = None + cli_excludes: list[str] = [] + cli_timing: bool = False + + def _parse_int(name: str, raw: str) -> int: + try: + v = int(raw) + except ValueError: + print(f"error: {name} must be a positive integer (got {raw!r})", file=sys.stderr) + sys.exit(2) + if v <= 0: + print(f"error: {name} must be > 0 (got {v})", file=sys.stderr) + sys.exit(2) + return v + + def _parse_float(name: str, raw: str) -> float: + try: + v = float(raw) + except ValueError: + print(f"error: {name} must be a positive number (got {raw!r})", file=sys.stderr) + sys.exit(2) + if v <= 0: + print(f"error: {name} must be > 0 (got {v})", file=sys.stderr) + sys.exit(2) + return v + + args = sys.argv[3:] if has_path else sys.argv[2:] + i = 0 + while i < len(args): + a = args[i] + if a == "--backend" and i + 1 < len(args): + backend = args[i + 1]; i += 2 + elif a.startswith("--backend="): + backend = a.split("=", 1)[1]; i += 1 + elif a == "--model" and i + 1 < len(args): + model = args[i + 1]; i += 2 + elif a.startswith("--model="): + model = a.split("=", 1)[1]; i += 1 + elif a == "--mode" and i + 1 < len(args): + extract_mode = args[i + 1]; i += 2 + elif a.startswith("--mode="): + extract_mode = a.split("=", 1)[1]; i += 1 + elif a == "--out" and i + 1 < len(args): + out_dir = Path(args[i + 1]); i += 2 + elif a.startswith("--out="): + out_dir = Path(a.split("=", 1)[1]); i += 1 + elif a == "--no-cluster": + no_cluster = True; i += 1 + elif a == "--dedup-llm": + dedup_llm = True; i += 1 + elif a == "--code-only": + code_only = True; i += 1 + elif a == "--google-workspace": + google_workspace = True; i += 1 + elif a == "--global": + global_merge = True; i += 1 + elif a == "--as" and i + 1 < len(args): + global_repo_tag = args[i + 1]; i += 2 + elif a == "--max-workers" and i + 1 < len(args): + cli_max_workers = _parse_int("--max-workers", args[i + 1]); i += 2 + elif a.startswith("--max-workers="): + cli_max_workers = _parse_int("--max-workers", a.split("=", 1)[1]); i += 1 + elif a == "--token-budget" and i + 1 < len(args): + cli_token_budget = _parse_int("--token-budget", args[i + 1]); i += 2 + elif a.startswith("--token-budget="): + cli_token_budget = _parse_int("--token-budget", a.split("=", 1)[1]); i += 1 + elif a == "--max-concurrency" and i + 1 < len(args): + cli_max_concurrency = _parse_int("--max-concurrency", args[i + 1]); i += 2 + elif a.startswith("--max-concurrency="): + cli_max_concurrency = _parse_int("--max-concurrency", a.split("=", 1)[1]); i += 1 + elif a == "--api-timeout" and i + 1 < len(args): + cli_api_timeout = _parse_float("--api-timeout", args[i + 1]); i += 2 + elif a.startswith("--api-timeout="): + cli_api_timeout = _parse_float("--api-timeout", a.split("=", 1)[1]); i += 1 + elif a == "--resolution" and i + 1 < len(args): + cli_resolution = _parse_float("--resolution", args[i + 1]); i += 2 + elif a.startswith("--resolution="): + cli_resolution = _parse_float("--resolution", a.split("=", 1)[1]); i += 1 + elif a == "--exclude-hubs" and i + 1 < len(args): + cli_exclude_hubs = float(args[i + 1]); i += 2 + elif a.startswith("--exclude-hubs="): + cli_exclude_hubs = float(a.split("=", 1)[1]); i += 1 + elif a == "--exclude" and i + 1 < len(args): + cli_excludes.append(args[i + 1]); i += 2 + elif a.startswith("--exclude="): + cli_excludes.append(a.split("=", 1)[1]); i += 1 + elif a == "--postgres" and i + 1 < len(args): + cli_postgres_dsn = args[i + 1]; i += 2 + elif a.startswith("--postgres="): + cli_postgres_dsn = a.split("=", 1)[1]; i += 1 + elif a == "--cargo": + cli_cargo = True + i += 1 + elif a == "--timing": + cli_timing = True; i += 1 + else: + i += 1 + + if not has_path and cli_postgres_dsn is None: + print("error: must specify a path to scan or a --postgres DSN", file=sys.stderr) + sys.exit(1) + + _VALID_MODES = {"deep"} + if extract_mode is not None and extract_mode not in _VALID_MODES: + print( + f"error: unknown --mode '{extract_mode}'. " + f"Available: {', '.join(sorted(_VALID_MODES))}", + file=sys.stderr, + ) + sys.exit(2) + deep_mode = extract_mode == "deep" + if deep_mode: + print("[graphify extract] deep mode enabled: richer semantic extraction") + + # CLI flag wins over env var. Setting GRAPHIFY_API_TIMEOUT here so + # _call_openai_compat picks it up without needing a new kwarg path. + if cli_api_timeout is not None: + os.environ["GRAPHIFY_API_TIMEOUT"] = str(cli_api_timeout) + if cli_max_workers is not None: + os.environ["GRAPHIFY_MAX_WORKERS"] = str(cli_max_workers) + + # Resolve output dir. The user-facing contract is "/graphify-out/" + # so a fresh checkout writes graphify-out/ at the project root, matching + # the skill.md pipeline. + out_root = (out_dir.resolve() if out_dir else target) + graphify_out = out_root / _GRAPHIFY_OUT + graphify_out.mkdir(parents=True, exist_ok=True) + + stages = _StageTimer(cli_timing) + + from graphify.detect import ( + detect as _detect, + detect_incremental as _detect_incremental, + save_manifest as _save_manifest, + ) + manifest_path = graphify_out / "manifest.json" + existing_graph_path = graphify_out / "graph.json" + incremental_mode = manifest_path.exists() and existing_graph_path.exists() if has_path else False + + if not has_path: + code_files = [] + doc_files = [] + paper_files = [] + image_files = [] + deleted_files = [] + unchanged_total = 0 + files_by_type = {} + elif incremental_mode: + print(f"[graphify extract] incremental scan of {target}") + detection = _detect_incremental( + target, + manifest_path=str(manifest_path), + google_workspace=google_workspace or None, + extra_excludes=cli_excludes or None, + ) + files_by_type = detection.get("files", {}) + new_by_type = detection.get("new_files", {}) + code_files = [Path(p) for p in new_by_type.get("code", [])] + doc_files = [Path(p) for p in new_by_type.get("document", [])] + paper_files = [Path(p) for p in new_by_type.get("paper", [])] + image_files = [Path(p) for p in new_by_type.get("image", [])] + deleted_files = list(detection.get("deleted_files", [])) + unchanged_total = sum(len(v) for v in detection.get("unchanged_files", {}).values()) + else: + print(f"[graphify extract] scanning {target}") + detection = _detect(target, google_workspace=google_workspace or None, extra_excludes=cli_excludes or None, cache_root=out_root) + files_by_type = detection.get("files", {}) + code_files = [Path(p) for p in files_by_type.get("code", [])] + doc_files = [Path(p) for p in files_by_type.get("document", [])] + paper_files = [Path(p) for p in files_by_type.get("paper", [])] + image_files = [Path(p) for p in files_by_type.get("image", [])] + deleted_files = [] + unchanged_total = 0 + + semantic_files = doc_files + paper_files + image_files + # --code-only: index code (pure local AST, no key) and skip the semantic + # (doc/paper/image) pass entirely, so a mixed repo doesn't hard-fail when no + # LLM backend is configured (#1734). Report what was skipped rather than + # silently dropping it. + if code_only and semantic_files: + print( + f"[graphify extract] --code-only: skipping {len(semantic_files)} " + f"non-code file(s) ({len(doc_files)} docs, {len(paper_files)} papers, " + f"{len(image_files)} images) — no LLM extraction" + ) + semantic_files = [] + doc_files = [] + paper_files = [] + image_files = [] + if incremental_mode: + print( + f"[graphify extract] {len(code_files)} code, {len(doc_files)} docs, " + f"{len(paper_files)} papers, {len(image_files)} images changed; " + f"{unchanged_total} unchanged; {len(deleted_files)} deleted" + ) + else: + print( + f"[graphify extract] found {len(code_files)} code, " + f"{len(doc_files)} docs, {len(paper_files)} papers, " + f"{len(image_files)} images" + ) + # Surface files that were seen but not classified (extensionless non-shebang + # project files like Dockerfile/Makefile, or unsupported extensions), so they + # are no longer invisible in graphify's own output (#1692). + _unclassified = detection.get("unclassified", []) if isinstance(detection, dict) else [] + if _unclassified: + _names = ", ".join(sorted({Path(p).name for p in _unclassified})[:6]) + _more = f" (+{len(_unclassified) - 6} more)" if len(_unclassified) > 6 else "" + print( + f"[graphify extract] {len(_unclassified)} file(s) not classified " + f"(no supported extension or shebang), skipped: {_names}{_more}" + ) + stages.mark("detect") + + # Resolve the LLM backend only now that we know whether the corpus + # needs one. A code-only corpus is pure local AST and must not require + # an API key; the key is enforced below only when there's LLM work. + from graphify.llm import ( + BACKENDS as _BACKENDS, + detect_backend as _detect_backend, + estimate_cost as _estimate_cost, + extract_corpus_parallel as _extract_corpus_parallel, + _format_backend_env_keys, + _get_backend_api_key, + ) + needs_llm = bool(semantic_files) or dedup_llm + auto_backend = backend is None and needs_llm + if backend is None and needs_llm: + backend = _detect_backend() + if backend is not None and backend not in _BACKENDS: + print( + f"error: unknown backend '{backend}'. " + f"Available: {', '.join(sorted(_BACKENDS))}", + file=sys.stderr, + ) + sys.exit(1) + if needs_llm: + if backend is None: + reasons = [] + if semantic_files: + reasons.append( + f"{len(semantic_files)} doc/paper/image file(s) need semantic extraction" + ) + if dedup_llm: + reasons.append("--dedup-llm was passed") + hint = "" + if semantic_files: + hint = (" Or pass --code-only to index just the code " + "(local AST, no key) and skip the non-code files.") + print( + "error: no LLM backend found (" + "; ".join(reasons) + "). " + "Graphify auto-detects local Ollama first (default model " + "qwen2.5-coder:3b, <=8B local safety class) and MiniMax as token-plan fallback. " + "Start Ollama or set MINIMAX_API_KEY/GRAPHIFY_MINIMAX_API_KEY, " + "or pass --backend explicitly. A code-only corpus needs no key." + hint, + file=sys.stderr, + ) + sys.exit(1) + if backend == "ollama": + from graphify.llm import _validate_ollama_base_url + _oll_url = os.environ.get("OLLAMA_BASE_URL", _BACKENDS["ollama"].get("base_url", "")) + try: + _validate_ollama_base_url(_oll_url, warn=False) + except ValueError as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(2) + if not _get_backend_api_key(backend): + allow_no_key = False + if backend == "ollama": + from urllib.parse import urlparse + ollama_url = os.environ.get( + "OLLAMA_BASE_URL", + _BACKENDS["ollama"].get("base_url", ""), + ) + try: + host = (urlparse(ollama_url).hostname or "").lower() + except Exception: + host = "" + allow_no_key = ( + host in ("localhost", "127.0.0.1", "::1") + or host.startswith("127.") + ) + elif backend == "bedrock": + allow_no_key = bool( + os.environ.get("AWS_PROFILE") + or os.environ.get("AWS_REGION") + or os.environ.get("AWS_DEFAULT_REGION") + or os.environ.get("AWS_ACCESS_KEY_ID") + ) + elif backend == "claude-cli": + import shutil as _shutil + allow_no_key = _shutil.which("claude") is not None + if not allow_no_key: + print( + "error: backend 'claude-cli' requires the `claude` CLI on $PATH " + "(install Claude Code and run `claude` once to authenticate).", + file=sys.stderr, + ) + sys.exit(1) + if not allow_no_key: + print( + f"error: backend '{backend}' requires {_format_backend_env_keys(backend)} to be set.", + file=sys.stderr, + ) + sys.exit(1) + + # AST extraction on code files. Empty code list (docs-only corpus) is + # the issue #698 case — skip cleanly instead of crashing inside extract(). + ast_result: dict = {"nodes": [], "edges": [], "input_tokens": 0, "output_tokens": 0} + if code_files: + from graphify.extract import extract as _ast_extract + # Anchor the cache at the output root, not the scanned project: + # with --out, a /graphify-out/cache/ would leak a + # graphify-out/ dir into a project that asked for external output. + ast_kwargs: dict = {"cache_root": out_root} + if cli_max_workers is not None: + ast_kwargs["max_workers"] = cli_max_workers + print(f"[graphify extract] AST extraction on {len(code_files)} code files...") + try: + ast_result = _ast_extract(code_files, **ast_kwargs) + except Exception as exc: + print(f"[graphify extract] AST extraction failed: {exc}", file=sys.stderr) + ast_result = {"nodes": [], "edges": [], "input_tokens": 0, "output_tokens": 0} + stages.mark("AST extract") + + # Semantic extraction on docs/papers/images. Check cache first. + from graphify.cache import ( + check_semantic_cache as _check_semantic_cache, + prune_semantic_cache as _prune_semantic_cache, + save_semantic_cache as _save_semantic_cache, + ) + sem_result: dict = { + "nodes": [], "edges": [], "hyperedges": [], + "input_tokens": 0, "output_tokens": 0, + } + sem_cache_hits = 0 + sem_cache_misses = 0 + if semantic_files: + sem_paths_str = [str(p) for p in semantic_files] + cached_nodes, cached_edges, cached_hyperedges, uncached_paths = ( + _check_semantic_cache(sem_paths_str, root=out_root) + ) + sem_cache_hits = len(semantic_files) - len(uncached_paths) + sem_cache_misses = len(uncached_paths) + sem_result["nodes"].extend(cached_nodes) + sem_result["edges"].extend(cached_edges) + sem_result["hyperedges"].extend(cached_hyperedges) + if sem_cache_hits: + print(f"[graphify extract] semantic cache: {sem_cache_hits} hit / {sem_cache_misses} miss") + + if uncached_paths: + print(f"[graphify extract] semantic extraction on {len(uncached_paths)} files via {backend}...") + corpus_kwargs: dict = { + "backend": backend, + "model": model, + "root": target, + "allow_minimax_fallback": auto_backend or backend == "ollama", + } + if deep_mode: + corpus_kwargs["deep_mode"] = True + if cli_token_budget is not None: + corpus_kwargs["token_budget"] = cli_token_budget + if cli_max_concurrency is not None: + corpus_kwargs["max_concurrency"] = cli_max_concurrency + + # Minimal progress callback so the CLI is no longer silent + # during long local-inference runs (issue #792 addendum). + # Also track per-chunk success so we can fail loudly when + # every chunk errors (e.g. missing backend SDK package). + _chunk_stats = {"total": 0, "succeeded": 0} + def _progress(idx: int, total: int, _result: dict) -> None: + _chunk_stats["total"] = total + _chunk_stats["succeeded"] += 1 + print( + f"[graphify extract] chunk {idx + 1}/{total} done", + flush=True, + ) + corpus_kwargs["on_chunk_done"] = _progress + + try: + fresh = _extract_corpus_parallel( + [Path(p) for p in uncached_paths], + **corpus_kwargs, + ) + except ImportError as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(1) + except Exception as exc: + print( + f"[graphify extract] semantic extraction failed: {exc}", + file=sys.stderr, + ) + fresh = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 0, "output_tokens": 0} + + if fresh.get("deferred_semantic"): + queue = graphify_out / "semantic-rebuild-queue.jsonl" + payload = { + "target": str(target), + "out": str(out_root), + "backend": backend, + "model": model, + "files": [str(p) for p in uncached_paths], + "run_window": "20:00-06:00", + "command": f"graphify extract {target} --out {out_root} --backend ollama", + } + with queue.open("a", encoding="utf-8") as fh: + fh.write(json.dumps(payload, sort_keys=True) + "\n") + print( + f"[graphify extract] semantic rebuild deferred; queued night job hint in {queue}", + file=sys.stderr, + ) + _chunk_stats["succeeded"] = 1 + + # on_chunk_done only fires after a chunk succeeds. If fresh + # semantic extraction was requested and no chunks completed, + # fail instead of writing an AST-only graph with exit 0. + if uncached_paths and _chunk_stats["succeeded"] == 0: + print( + f"[graphify extract] error: all semantic chunks failed " + f"for backend '{backend}' ({len(uncached_paths)} uncached files) - " + f"see per-chunk errors above. If you see 'requires the X package', " + f"run `pip install X` and retry.", + file=sys.stderr, + ) + sys.exit(1) + try: + _save_semantic_cache( + fresh.get("nodes", []), + fresh.get("edges", []), + fresh.get("hyperedges", []), + root=out_root, + allowed_source_files=uncached_paths, + ) + except Exception as exc: + print(f"[graphify extract] warning: could not write semantic cache: {exc}", file=sys.stderr) + sem_result["nodes"].extend(fresh.get("nodes", [])) + sem_result["edges"].extend(fresh.get("edges", [])) + sem_result["hyperedges"].extend(fresh.get("hyperedges", [])) + sem_result["input_tokens"] += fresh.get("input_tokens", 0) + sem_result["output_tokens"] += fresh.get("output_tokens", 0) + + # Prune orphaned semantic cache entries. The semantic cache is + # content-hash-keyed and unversioned, so it is never swept by the AST + # version-cleanup: every content change or file deletion leaves a + # permanent orphan that accumulates unbounded (#1527). Sweep it against + # the FULL live document set (``files_by_type`` — present in both the + # incremental and full branches), NOT the incremental ``semantic_files`` + # changed-subset, which would delete every unchanged doc's valid entry. + # Best-effort: a prune failure must never break extraction. + try: + from graphify.cache import file_hash as _file_hash + _live_hashes: set[str] = set() + for _kind in ("document", "paper", "image"): + for _fp in files_by_type.get(_kind, []): + _abs = Path(_fp) + if not _abs.is_absolute(): + _abs = Path(out_root) / _abs + if not _abs.is_file(): + continue # deleted/missing — leave out so its entry is pruned + try: + _live_hashes.add(_file_hash(_abs, out_root)) + except OSError: + pass + _prune_semantic_cache(out_root, _live_hashes) + except Exception as exc: + print(f"[graphify extract] warning: could not prune semantic cache: {exc}", file=sys.stderr) + stages.mark("semantic extract") + + pg_result: dict = {"nodes": [], "edges": []} + if cli_postgres_dsn is not None: + from graphify.pg_introspect import introspect_postgres + print(f"[graphify extract] introspecting PostgreSQL schema...") + try: + pg_result = introspect_postgres(cli_postgres_dsn) + except (ConnectionError, ImportError) as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(1) + print(f"[graphify extract] PostgreSQL: {len(pg_result['nodes'])} nodes, " + f"{len(pg_result['edges'])} edges") + + cargo_result: dict = {"nodes": [], "edges": []} + if cli_cargo: + from graphify.cargo_introspect import introspect_cargo + print("[graphify extract] introspecting Cargo workspace...") + try: + cargo_result = introspect_cargo(target) + except (ConnectionError, ImportError, OSError) as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(1) + print(f"[graphify extract] Cargo: {len(cargo_result['nodes'])} nodes, " + f"{len(cargo_result['edges'])} edges") + + # Merge AST + semantic + pg_result + cargo_result. Order matters for deduplication: passing AST + # first means semantic node attributes win on collision (richer labels + # for symbols also referenced in docs). Hyperedges only come from the + # semantic side. + merged: dict = { + "nodes": list(ast_result.get("nodes", [])) + list(sem_result.get("nodes", [])) + list(pg_result.get("nodes", [])) + list(cargo_result.get("nodes", [])), + "edges": list(ast_result.get("edges", [])) + list(sem_result.get("edges", [])) + list(pg_result.get("edges", [])) + list(cargo_result.get("edges", [])), + "hyperedges": list(sem_result.get("hyperedges", [])), + "input_tokens": ast_result.get("input_tokens", 0) + sem_result.get("input_tokens", 0), + "output_tokens": ast_result.get("output_tokens", 0) + sem_result.get("output_tokens", 0), + } + + graph_json_path = graphify_out / "graph.json" + analysis_path = graphify_out / ".graphify_analysis.json" + + # Build a manifest-safe files dict: only stamp semantic_hash for files + # that actually produced output (cache hit or fresh extraction). Files + # whose chunk failed have no source_file entry in sem_result — leaving + # their semantic_hash empty so detect_incremental re-queues them (#933). + _sem_extracted: set[str] = { + n.get("source_file", "") for n in sem_result.get("nodes", []) + } | { + e.get("source_file", "") for e in sem_result.get("edges", []) + } + _sem_extracted.discard("") + _sem_types = {"document", "paper", "image"} + _manifest_files = { + ftype: [f for f in flist if ftype not in _sem_types or f in _sem_extracted] + for ftype, flist in files_by_type.items() + } + + if no_cluster: + # --no-cluster: dump the raw merged extraction as graph.json. + # No NetworkX, no community detection, no analysis sidecar. + # Dedupe nodes (by id) and parallel edges so the raw output matches the + # clustered path (whose DiGraph collapses both) and stays deterministic + # across modes (#1317; node dedup also collapses shared Swift module + # anchors emitted per importing file, #1327). + from graphify.build import dedupe_edges as _dedupe_edges, dedupe_nodes as _dedupe_nodes + from graphify.export import backup_if_protected as _backup + if ( + incremental_mode + and not code_files + and not semantic_files + and not deleted_files + and not pg_result.get("nodes") + and not pg_result.get("edges") + and not cargo_result.get("nodes") + and not cargo_result.get("edges") + ): + print( + "[graphify extract] no incremental changes detected " + "(--no-cluster); outputs left untouched." + ) + try: + _save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both", root=target) + except Exception as exc: + print(f"[graphify extract] warning: could not write manifest: {exc}", file=sys.stderr) + stages.total() + sys.exit(0) + + merged["nodes"] = _dedupe_nodes(merged["nodes"]) + merged["edges"] = _dedupe_edges(merged["edges"]) + # Backfill source_file from endpoint nodes — this raw path bypasses + # build_from_json's backfill, and semantic edges sometimes omit it (#1279). + _node_sf = {n.get("id"): n.get("source_file") for n in merged["nodes"]} + for _e in merged["edges"]: + if not _e.get("source_file"): + _e["source_file"] = ( + _node_sf.get(_e.get("source")) or _node_sf.get(_e.get("target")) or "" + ) + _backup(graphify_out) + graph_json_path.write_text( + json.dumps(merged, indent=2), encoding="utf-8" + ) + stages.mark("write") + cost = _estimate_cost( + backend, merged["input_tokens"], merged["output_tokens"] + ) + print( + f"[graphify extract] wrote {graph_json_path} — " + f"{len(merged['nodes'])} nodes, {len(merged['edges'])} edges " + f"(no clustering)" + ) + if merged["input_tokens"] or merged["output_tokens"]: + print( + f"[graphify extract] tokens: " + f"{merged['input_tokens']:,} in / " + f"{merged['output_tokens']:,} out, " + f"est. cost: ${cost:.4f}" + ) + try: + _save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both", root=target) + except Exception as exc: + print(f"[graphify extract] warning: could not write manifest: {exc}", file=sys.stderr) + if global_merge: + from graphify.global_graph import global_add as _global_add + _tag = global_repo_tag or target.name + try: + result = _global_add(graphify_out / "graph.json", _tag) + if result["skipped"]: + print(f"[graphify global] '{_tag}' unchanged since last add - skipped.") + else: + print(f"[graphify global] '{_tag}' merged into global graph " + f"(+{result['nodes_added']} nodes, -{result['nodes_removed']} pruned).") + except Exception as exc: + print(f"[graphify global] warning: failed to merge into global graph: {exc}", file=sys.stderr) + stages.total() + sys.exit(0) + + # Build graph + cluster + score + write. + from graphify.build import ( + build as _build, + build_from_json as _build_from_json, + build_merge as _build_merge, + ) + from graphify.cluster import cluster as _cluster, score_all as _score_all + from graphify.export import to_json as _to_json + from graphify.analyze import god_nodes as _god_nodes, surprising_connections as _surprising + dedup_backend = backend if dedup_llm else None + if incremental_mode: + G = _build_merge( + [merged], + graph_path=existing_graph_path, + prune_sources=deleted_files or None, + dedup=True, + dedup_llm_backend=dedup_backend, + root=target, + ) + else: + G = _build([merged], dedup=True, dedup_llm_backend=dedup_backend, root=target) + stages.mark("build") + if G.number_of_nodes() == 0: + print( + "[graphify extract] graph is empty — extraction produced no nodes. " + "Possible causes: all files skipped, binary-only corpus, or LLM " + "returned no edges.", + file=sys.stderr, + ) + sys.exit(1) + + communities = _cluster(G, resolution=cli_resolution, exclude_hubs_percentile=cli_exclude_hubs) + stages.mark("cluster") + cohesion = _score_all(G, communities) + try: + gods = _god_nodes(G) + except Exception: + gods = [] + try: + surprises = _surprising(G, communities) + except Exception: + surprises = [] + stages.mark("analyze") + + from graphify.export import backup_if_protected as _backup + _backup(graphify_out) + _to_json(G, communities, str(graph_json_path), force=True) + stages.mark("export") + if merged.get("output_tokens", 0) > 0: + (graphify_out / ".graphify_semantic_marker").write_text( + json.dumps({"output_tokens": merged["output_tokens"]}), encoding="utf-8" + ) + if global_merge: + from graphify.global_graph import global_add as _global_add + _tag = global_repo_tag or target.name + try: + result = _global_add(graphify_out / "graph.json", _tag) + if result["skipped"]: + print(f"[graphify global] '{_tag}' unchanged since last add - skipped.") + else: + print(f"[graphify global] '{_tag}' merged into global graph " + f"(+{result['nodes_added']} nodes, -{result['nodes_removed']} pruned).") + except Exception as exc: + print(f"[graphify global] warning: failed to merge into global graph: {exc}", file=sys.stderr) + analysis = { + "communities": {str(k): v for k, v in communities.items()}, + "cohesion": {str(k): v for k, v in cohesion.items()}, + "gods": gods, + "surprises": surprises, + "tokens": { + "input": merged["input_tokens"], + "output": merged["output_tokens"], + }, + } + analysis_path.write_text(json.dumps(analysis, indent=2), encoding="utf-8") + try: + _save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both", root=target) + except Exception as exc: + print(f"[graphify extract] warning: could not write manifest: {exc}", file=sys.stderr) + + cost = _estimate_cost(backend, merged["input_tokens"], merged["output_tokens"]) + print( + f"[graphify extract] wrote {graph_json_path}: " + f"{G.number_of_nodes()} nodes, {G.number_of_edges()} edges, " + f"{len(communities)} communities" + ) + print(f"[graphify extract] wrote {analysis_path}") + if incremental_mode: + print( + f"[graphify extract] incremental summary: " + f"{sem_cache_hits + unchanged_total} files cached/unchanged, " + f"{len(code_files) + sem_cache_misses} re-extracted, " + f"{len(deleted_files)} deleted" + ) + elif sem_cache_hits: + print(f"[graphify extract] semantic cache: {sem_cache_hits} cached, {sem_cache_misses} re-extracted") + if merged["input_tokens"] or merged["output_tokens"]: + print( + f"[graphify extract] tokens: " + f"{merged['input_tokens']:,} in / " + f"{merged['output_tokens']:,} out, " + f"est. cost (~{backend}): ${cost:.4f}" + ) + # extract intentionally stops at graph.json + analysis; the report and + # community labels are produced by `cluster-only` (or an agent's Step 5). + # Point standalone users at it so communities get named (#1097). + print( + "[graphify extract] next: run " + f"`graphify cluster-only {graphify_out.parent}` " + "to generate GRAPH_REPORT.md and name communities" + ) + stages.total() + + elif cmd == "cache-check": + # graphify cache-check [--root ] + # Reads file paths (one per line) from , checks semantic cache. + # Writes: + # graphify-out/.graphify_cached.json — already-cached nodes/edges/hyperedges + # graphify-out/.graphify_uncached.txt — paths that need extraction + # Stdout: "Cache: N hit, M miss" + from graphify.cache import check_semantic_cache + if len(sys.argv) < 3: + print("Usage: graphify cache-check [--root ]", file=sys.stderr) + sys.exit(1) + files_from = Path(sys.argv[2]) + root = Path(".") + i = 3 + while i < len(sys.argv): + if sys.argv[i] == "--root" and i + 1 < len(sys.argv): + root = Path(sys.argv[i + 1]) + i += 2 + else: + i += 1 + files = [f for f in files_from.read_text(encoding="utf-8").splitlines() if f.strip()] + cached_nodes, cached_edges, cached_hyperedges, uncached = check_semantic_cache(files, root) + out = root / _GRAPHIFY_OUT + out.mkdir(parents=True, exist_ok=True) + if cached_nodes or cached_edges or cached_hyperedges: + (out / ".graphify_cached.json").write_text( + json.dumps({"nodes": cached_nodes, "edges": cached_edges, "hyperedges": cached_hyperedges}, + ensure_ascii=False), + encoding="utf-8", + ) + (out / ".graphify_uncached.txt").write_text("\n".join(uncached), encoding="utf-8") + print(f"Cache: {len(files) - len(uncached)} hit, {len(uncached)} miss") + + elif cmd == "merge-chunks": + # graphify merge-chunks --out + # Concatenates .graphify_chunk_*.json files written by semantic subagents. + # Deduplicates nodes by id (first writer wins). Sums token counts. + import glob as _glob + if len(sys.argv) < 3: + print("Usage: graphify merge-chunks --out ", file=sys.stderr) + sys.exit(1) + out_path: Path | None = None + chunk_args: list[str] = [] + i = 2 + while i < len(sys.argv): + if sys.argv[i] == "--out" and i + 1 < len(sys.argv): + out_path = Path(sys.argv[i + 1]) + i += 2 + else: + chunk_args.append(sys.argv[i]) + i += 1 + if not out_path: + print("error: --out required", file=sys.stderr) + sys.exit(1) + chunk_files: list[str] = [] + for arg in chunk_args: + expanded = _glob.glob(arg) + chunk_files.extend(sorted(expanded) if expanded else [arg]) + merged: dict = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 0, "output_tokens": 0} + seen_ids: set[str] = set() + for cf in chunk_files: + try: + chunk = json.loads(Path(cf).read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError) as exc: + print(f"[graphify merge-chunks] warning: skipping {cf}: {exc}", file=sys.stderr) + continue + for n in chunk.get("nodes", []): + if n.get("id") not in seen_ids: + seen_ids.add(n["id"]) + merged["nodes"].append(n) + merged["edges"].extend(chunk.get("edges", [])) + merged["hyperedges"].extend(chunk.get("hyperedges", [])) + merged["input_tokens"] += chunk.get("input_tokens", 0) + merged["output_tokens"] += chunk.get("output_tokens", 0) + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(json.dumps(merged, ensure_ascii=False), encoding="utf-8") + print( + f"Merged {len(chunk_files)} chunks: {len(merged['nodes'])} nodes, {len(merged['edges'])} edges, " + f"{merged['input_tokens']:,} in / {merged['output_tokens']:,} out tokens" + ) + + elif cmd == "merge-semantic": + # graphify merge-semantic --cached --new --out + # Merges cached semantic results with freshly-extracted chunk results. + # Deduplicates nodes by id (cached entries take priority over new ones). + if len(sys.argv) < 3: + print("Usage: graphify merge-semantic --cached --new --out ", file=sys.stderr) + sys.exit(1) + cached_path: Path | None = None + new_path: Path | None = None + out_path2: Path | None = None + i = 2 + while i < len(sys.argv): + if sys.argv[i] == "--cached" and i + 1 < len(sys.argv): + cached_path = Path(sys.argv[i + 1]); i += 2 + elif sys.argv[i] == "--new" and i + 1 < len(sys.argv): + new_path = Path(sys.argv[i + 1]); i += 2 + elif sys.argv[i] == "--out" and i + 1 < len(sys.argv): + out_path2 = Path(sys.argv[i + 1]); i += 2 + else: + i += 1 + if not out_path2: + print("error: --out required", file=sys.stderr) + sys.exit(1) + empty: dict = {"nodes": [], "edges": [], "hyperedges": []} + cached_data = json.loads(cached_path.read_text(encoding="utf-8")) if cached_path and cached_path.exists() else empty + new_data = json.loads(new_path.read_text(encoding="utf-8")) if new_path and new_path.exists() else empty + seen_ids2: set[str] = set() + all_nodes: list[dict] = [] + for n in cached_data.get("nodes", []) + new_data.get("nodes", []): + if n.get("id") not in seen_ids2: + seen_ids2.add(n["id"]) + all_nodes.append(n) + merged2 = { + "nodes": all_nodes, + "edges": cached_data.get("edges", []) + new_data.get("edges", []), + "hyperedges": cached_data.get("hyperedges", []) + new_data.get("hyperedges", []), + } + out_path2.parent.mkdir(parents=True, exist_ok=True) + out_path2.write_text(json.dumps(merged2, ensure_ascii=False), encoding="utf-8") + print(f"Merged: {len(merged2['nodes'])} nodes, {len(merged2['edges'])} edges") + + elif Path(cmd).exists() or cmd in (".", "..") or cmd.startswith(("./", "../", "/", "~")): + # User ran `graphify ` directly — treat as `graphify extract `. + # Common when following the PowerShell note in README (`graphify .`) or + # copy-pasting skill invocations without the leading slash. + sys.argv.insert(2, sys.argv[1]) + sys.argv[1] = "extract" + _reenter_main() + else: + print(f"error: unknown command '{cmd}'", file=sys.stderr) + print("Run 'graphify --help' for usage.", file=sys.stderr) + sys.exit(1) diff --git a/graphify/cluster.py b/graphify/cluster.py index 82f063d21..682210700 100644 --- a/graphify/cluster.py +++ b/graphify/cluster.py @@ -83,6 +83,54 @@ def _partition(G: nx.Graph, resolution: float = 1.0) -> dict[str, int]: _COHESION_SPLIT_MIN_SIZE = 50 # only cohesion-split if community has at least this many nodes +def label_communities_by_hub( + G: nx.Graph, communities: dict[int, list[str]] +) -> dict[int, str]: + """Deterministic, LLM-free community labels: name each community after its + highest-degree member — the structural hub — so a report reads ``auth`` / + ``log_action`` instead of ``Community 70``. Degree is measured on the full graph + ``G``; ties break by node id for run-to-run stability. A community whose members + are all absent from ``G`` falls back to ``Community {cid}``. + + Used as the default (no-backend) labeler; an LLM naming pass, when configured, + overrides these with richer names. + """ + labels: dict[int, str] = {} + for cid, members in communities.items(): + present = [n for n in members if n in G] + if not present: + labels[cid] = f"Community {cid}" + continue + # highest degree wins; ties broken by node id (ascending) for determinism + hub = min(present, key=lambda n: (-G.degree(n), str(n))) + name = str(G.nodes[hub].get("label") or hub).strip() + if name.endswith("()"): + name = name[:-2] + labels[cid] = name or f"Community {cid}" + return labels + + +def community_member_sigs(communities: dict[int, list[str]]) -> dict[int, str]: + """Per-community membership fingerprints: ``{cid: sha256(sorted member ids)}``. + + Persisted next to ``.graphify_labels.json`` so a later ``cluster-only`` can tell + which communities actually changed since labeling. A cid whose members no longer + hash the same is a different community — reusing its old (LLM) label there is the + "stale label after re-scoping" bug this guards against. Deterministic; independent + of cid index, node order, and machine. + """ + import hashlib + + sigs: dict[int, str] = {} + for cid, members in communities.items(): + h = hashlib.sha256() + for nid in sorted(str(n) for n in members): + h.update(nid.encode("utf-8", "replace")) + h.update(b"\x00") + sigs[cid] = h.hexdigest()[:16] + return sigs + + def cluster( G: nx.Graph, resolution: float = 1.0, diff --git a/graphify/dedup.py b/graphify/dedup.py index d02aedcc0..e0ddd2e3b 100644 --- a/graphify/dedup.py +++ b/graphify/dedup.py @@ -6,6 +6,7 @@ from __future__ import annotations import math import re +import sys import unicodedata from collections import defaultdict @@ -219,12 +220,29 @@ def deduplicate_entities( if len(nodes) <= 1: return nodes, edges - # Pre-deduplicate: keep first occurrence of each id + # Pre-deduplicate: keep first occurrence of each id. + # Warn when two nodes share an ID but originate from different source files — + # this indicates a cross-chunk ID collision (#1504) where silent data loss occurs. seen_ids: dict[str, dict] = {} for node in nodes: nid = node.get("id", "") - if nid and nid not in seen_ids: + if not nid: + continue + if nid not in seen_ids: seen_ids[nid] = node + else: + existing_sf = seen_ids[nid].get("source_file") or "" + new_sf = node.get("source_file") or "" + if existing_sf != new_sf: + print( + f"[graphify] WARNING: node '{nid}' from '{new_sf}' collides with " + f"node from '{existing_sf}' — the second node will be dropped. " + f"This is a cross-chunk ID collision caused by two files with the " + f"same name in different directories. To avoid data loss, run " + f"'graphify extract' per subfolder and merge with " + f"'graphify merge-graphs'.", + file=sys.stderr, + ) unique_nodes = list(seen_ids.values()) if len(unique_nodes) <= 1: diff --git a/graphify/detect.py b/graphify/detect.py index dba669e87..6526b97b4 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -27,7 +27,7 @@ class FileType(str, Enum): _MANIFEST_PATH = str(out_path("manifest.json")) -CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger'} +CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.rake', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger'} DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.txt', '.rst', '.html', '.yaml', '.yml'} PAPER_EXTENSIONS = {'.pdf'} IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'} @@ -125,6 +125,15 @@ def _zip_within_caps(path: Path) -> bool: re.compile(r'(? bool: name = path.name if any(p.search(name) for p in _SENSITIVE_PATTERNS): return True - # Stage 3: generic keywords, only when load-bearing in the name - return _generic_keyword_hit(name) + # Stage 3: generic keywords, only when load-bearing in the name. Do NOT let a + # bare name keyword silently drop a genuine programming-language source file: + # a .rb/.py named device_token or passwords_controller is a module, not a secret + # store (#1666). Data/config formats (.json, .yaml, .toml, ...) are deliberately + # NOT exempt even though .json routes through the CODE path for manifest parsing, + # because credentials.json / oauth_token.json / secrets.yaml are exactly the + # secret stores this stage must catch. The specific Stage 2 patterns (.env, .pem, + # id_rsa, ...) still apply to everything regardless of extension. + if _generic_keyword_hit(name): + ext = path.suffix.lower() + is_source_code = classify_file(path) == FileType.CODE and ext not in _SECRET_PRONE_DATA_EXTS + return not is_source_code + return False def _looks_like_paper(path: Path) -> bool: @@ -630,11 +650,19 @@ def convert_office_file(path: Path, out_dir: Path) -> Path | None: normalized_path = unicodedata.normalize("NFC", str(path.resolve())) name_hash = hashlib.sha256(normalized_path.encode()).hexdigest()[:8] out_path = out_dir / f"{path.stem}_{name_hash}.md" - # Once the hash is stable the sidecar name is deterministic; skip re-writing - # an existing sidecar so an unchanged source never churns its mtime (which - # would still flag it as changed in detect_incremental). - if out_path.exists(): - return out_path + # Skip re-writing only when the sidecar is present AND at least as new as the + # source. detect_incremental tracks the SIDECAR (not the Office source), so a + # sidecar that is never rewritten after the source changes leaves the doc + # reported "unchanged" forever and freezes the graph (#1649). Re-converting + # when the source is newer bumps the sidecar's mtime/content, which the + # incremental hash check then correctly picks up. An unchanged source keeps + # its (newer-or-equal) sidecar untouched so it never churns (#1226). + try: + if out_path.exists() and os.stat(_os_path(out_path)).st_mtime >= os.stat(_os_path(path)).st_mtime: + return out_path + except OSError: + if out_path.exists(): + return out_path out_path.write_text( f"\n\n{text}", encoding="utf-8", @@ -651,7 +679,8 @@ def count_words(path: Path) -> int: return len(docx_to_markdown(path).split()) if ext == ".xlsx": return len(xlsx_to_markdown(path).split()) - return len(path.read_text(encoding="utf-8", errors="ignore").split()) + with open(_os_path(path), encoding="utf-8", errors="ignore") as f: + return len(f.read().split()) except Exception: return 0 @@ -663,12 +692,12 @@ def count_words(path: Path) -> int: "dist", "build", "target", "out", "site-packages", "lib64", ".pytest_cache", ".mypy_cache", ".ruff_cache", - ".tox", ".eggs", "*.egg-info", + ".tox", ".nox", ".eggs", "*.egg-info", # nox is tox's successor, same .nox/ venv shape (#1804) "graphify-out", GRAPHIFY_OUT_NAME, # never treat own output as source input (#524); honour GRAPHIFY_OUT (#1423) # Coverage/test-artefact dirs — generated, never architecturally meaningful "coverage", "lcov-report", # Vitest/Istanbul/nyc HTML reports (#870) "visual-tests", "visual-test", # Playwright/visual-regression bundles (#869) - "__snapshots__", "snapshots", # Jest/Vitest snapshot dirs + "__snapshots__", # Jest/Vitest snapshot dir (unambiguous) "storybook-static", # Storybook production build output "dist-protected", # Protected dist variants (same noise as dist) # Framework cache/build dirs — generated, never architecturally meaningful (#873) @@ -687,10 +716,31 @@ def count_words(path: Path) -> int: "composer.lock", "go.sum", "go.work.sum", } +# A bare "snapshots" dir is a Jest/Vitest artifact only when it actually holds +# snapshot files or lives directly under a JS test root. Elsewhere it is often a +# real code namespace (e.g. Rails app/services/snapshots/), so pruning it by name +# silently dropped legitimate source from the graph (#1666). "__snapshots__" stays +# unconditionally pruned above; only the ambiguous bare name is gated here. +_JS_SNAPSHOT_TEST_ROOTS = frozenset({"__tests__", "__test__"}) + + def _is_noise_dir(part: str, parent: "Path | None" = None) -> bool: """Return True if this directory name looks like a venv, cache, or dep dir.""" if part in _SKIP_DIRS: return True + if part == "snapshots": + # Prune only when it looks like an actual JS/Vitest snapshot dir. + if parent is None: + return False # cannot verify; keep a possibly-real code dir + snap_dir = parent / part + if parent.name in _JS_SNAPSHOT_TEST_ROOTS: + return True + try: + if next(snap_dir.glob("*.snap"), None) is not None: + return True + except OSError: + pass + return False # Catch *_venv, *_repo/site-packages patterns if part.endswith("_venv") or part.endswith("_env"): return True @@ -742,12 +792,57 @@ def _find_vcs_root(start: Path) -> Path | None: current = parent +def _git_info_exclude(vcs_root: Path) -> Path | None: + """Resolve ``$GIT_DIR/info/exclude`` for the repo rooted at ``vcs_root``. + + ``info/exclude`` is where git records local-only, uncommitted excludes — and + where ``git worktree add`` writes nested worktree paths — so a repo can ignore + a directory without any ``.gitignore`` entry. graphify only read + ``.gitignore``/``.graphifyignore``, so it walked into those worktree copies and + the graph exploded (#1810). Handles the linked-worktree/submodule case where + ``.git`` is a file (``gitdir: ``) and the real excludes live in the + shared common git dir. Returns None when there is no readable exclude file. + """ + dot_git = vcs_root / ".git" + git_dir: Path | None = None + if dot_git.is_dir(): + git_dir = dot_git + elif dot_git.is_file(): + try: + content = dot_git.read_text(encoding="utf-8", errors="ignore").strip() + except OSError: + content = "" + if content.startswith("gitdir:"): + gd = Path(content[len("gitdir:"):].strip()) + if not gd.is_absolute(): + gd = (vcs_root / gd).resolve() + git_dir = gd + # A linked worktree's gitdir holds a `commondir` file pointing at the + # shared git dir, where info/exclude actually lives. + commondir = gd / "commondir" + if commondir.exists(): + try: + cd_raw = commondir.read_text(encoding="utf-8", errors="ignore").strip() + except OSError: + cd_raw = "" + if cd_raw: + cd = Path(cd_raw) + git_dir = cd if cd.is_absolute() else (gd / cd).resolve() + if git_dir is None: + return None + exclude = git_dir / "info" / "exclude" + return exclude if exclude.is_file() else None + + def _load_graphifyignore(root: Path) -> list[tuple[Path, str]]: - """Read .gitignore + .graphifyignore rules and return (anchor_dir, pattern). + """Read .graphifyignore files and return (anchor_dir, pattern) pairs. + + Patterns are returned outer-first so that inner (closer) rules are + appended last and win via last-match-wins semantics — matching gitignore + behavior exactly. - .gitignore gives the project owner's broad "not source" signal (datasets, - logs, vendored clones). .graphifyignore is appended after it so graphify- - specific rules still win by normal last-match-wins semantics. + Walk ceiling: the nearest VCS root if inside a repo, otherwise the scan + root itself (hermetic — no leakage across unrelated sibling projects). """ root = root.resolve() ceiling = _find_vcs_root(root) or root @@ -763,6 +858,18 @@ def _load_graphifyignore(root: Path) -> list[tuple[Path, str]]: dirs.reverse() # ceiling first, scan root last patterns: list[tuple[Path, str]] = [] + + # $GIT_DIR/info/exclude is repo-root-scoped and, per git, ranks below every + # per-directory .gitignore/.graphifyignore — so load it first (lowest priority + # under last-match-wins) anchored at the VCS root, letting a nearer `!` + # re-include still override it (#1810). + info_exclude = _git_info_exclude(ceiling) + if info_exclude is not None: + for raw in info_exclude.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = _parse_gitignore_line(raw) + if line: + patterns.append((ceiling, line)) + for d in dirs: # Merge .gitignore and .graphifyignore for this dir (#1363). Previously # the presence of a .graphifyignore made graphify skip that dir's @@ -990,14 +1097,11 @@ def _could_contain_included_path(path: Path, root: Path, patterns: list[tuple[Pa def _auto_follow_symlinks(root: Path) -> bool: - """Auto-detect: ``True`` if ``root`` has any direct symlinked child. + """Return whether ``root`` has any direct symlinked child. - Allows "fake working dir" patterns (e.g. a folder full of symlinks pointing - at scattered source dirs across the user's machine) to work transparently - without the caller having to know to pass ``follow_symlinks=True``. - - Override is always possible by passing an explicit ``follow_symlinks=True`` - or ``follow_symlinks=False`` to :func:`detect` / :func:`detect_incremental`. + Kept for callers that import the private helper, but detection no longer + enables symlink following automatically. Following symlinks is now an + explicit opt-in, and out-of-root symlink targets are never indexed. """ try: for p in root.iterdir(): @@ -1008,10 +1112,19 @@ def _auto_follow_symlinks(root: Path) -> bool: return False -def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: bool | None = None, extra_excludes: list[str] | None = None, count_content: bool = True) -> dict: +def _resolves_under_root(path: Path, root: Path) -> bool: + """True when ``path`` resolves to a target inside ``root``.""" + try: + path.resolve().relative_to(root.resolve()) + except (OSError, RuntimeError, ValueError): + return False + return True + + +def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: bool | None = None, extra_excludes: list[str] | None = None, cache_root: Path | None = None, count_content: bool = True) -> dict: root = root.resolve() if follow_symlinks is None: - follow_symlinks = _auto_follow_symlinks(root) + follow_symlinks = False google_workspace = google_workspace_enabled() if google_workspace is None else google_workspace files: dict[FileType, list[str]] = { FileType.CODE: [], @@ -1022,7 +1135,16 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: } total_words = 0 + def _wc(path: Path) -> int: + # Cache word counts against each file's stat signature so unchanged + # PDFs/docx aren't re-parsed on every run just to size the corpus (#1656). + # cache_root (when given, e.g. from `extract --out`) keeps this cache out + # of the scanned corpus (#1747). + from graphify import cache as _cache + return _cache.cached_word_count(path, root, count_words, cache_root=cache_root) + skipped_sensitive: list[str] = [] + unclassified: list[str] = [] ignore_patterns = _load_graphifyignore(root) ignore_cache: dict[Path, bool] = {} # shared across all _is_ignored calls in this scan # CLI --exclude patterns are anchored at the scan root and appended last @@ -1043,9 +1165,29 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: seen: set[Path] = set() all_files: list[Path] = [] + # os.walk swallows os.scandir errors by default (no onerror -> the failing + # directory subtree is silently skipped). That turns a transient + # PermissionError, or a directory created/deleted mid-walk (e.g. concurrent + # writes racing the scan), into a partial file list and, downstream, a + # silently partial graph.json. Record and surface every skipped directory + # so an incomplete enumeration is visible rather than silent. + walk_errors: list[str] = [] + + def _on_walk_error(err: OSError) -> None: + import sys as _sys + target = getattr(err, "filename", None) or "" + walk_errors.append(f"{target}: {err}") + print( + f"[graphify] WARNING: could not scan {target} ({err}); " + f"its files are missing from this run's enumeration.", + file=_sys.stderr, + ) + for scan_root in scan_paths: in_memory_tree = memory_dir.exists() and str(scan_root).startswith(str(memory_dir)) - for dirpath, dirnames, filenames in os.walk(scan_root, followlinks=follow_symlinks): + for dirpath, dirnames, filenames in os.walk( + scan_root, followlinks=follow_symlinks, onerror=_on_walk_error + ): dp = Path(dirpath) if follow_symlinks and os.path.islink(dirpath): real = os.path.realpath(dirpath) @@ -1071,6 +1213,15 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: if not _is_noise_dir(d, dp) and not _is_ignored(dp / d, root, ignore_patterns, _cache=ignore_cache) ] + if follow_symlinks: + safe_dirs: list[str] = [] + for d in dirnames: + child = dp / d + if child.is_symlink() and not _resolves_under_root(child, root): + skipped_sensitive.append(str(child) + " [symlink target outside scan root]") + continue + safe_dirs.append(d) + dirnames[:] = safe_dirs for fname in filenames: if fname in _SKIP_FILES: continue @@ -1092,10 +1243,20 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: continue if not in_memory and _is_ignored(p, root, ignore_patterns, _cache=ignore_cache): continue + if not _resolves_under_root(p, root): + skipped_sensitive.append(str(p) + " [symlink target outside scan root]") + continue if _is_sensitive(p): skipped_sensitive.append(str(p)) continue ftype = classify_file(p) + if not ftype: + # Considered but unclassifiable: an extension not in any supported set, + # or an extensionless, non-shebang file (Dockerfile, Gemfile, Makefile, + # Rakefile, LICENSE, ...). Previously these left no trace at all — not + # counted, not listed — so a user couldn't tell they were seen (#1692). + unclassified.append(str(p)) + continue if ftype: if p.suffix.lower() in GOOGLE_WORKSPACE_EXTENSIONS: if not google_workspace: @@ -1114,7 +1275,8 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: if _is_ignored(md_path, root, ignore_patterns, _cache=ignore_cache): continue files[ftype].append(str(md_path)) - total_words += count_words(md_path) + if count_content: + total_words += _wc(md_path) else: skipped_sensitive.append(str(p) + " [Google Workspace export produced no readable text]") continue @@ -1125,14 +1287,15 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: if _is_ignored(md_path, root, ignore_patterns, _cache=ignore_cache): continue files[ftype].append(str(md_path)) - total_words += count_words(md_path) + if count_content: + total_words += _wc(md_path) else: # Conversion failed (library not installed) - skip with note skipped_sensitive.append(str(p) + " [office conversion failed - pip install graphifyy[office]]") continue files[ftype].append(str(p)) if count_content and ftype != FileType.VIDEO: - total_words += count_words(p) + total_words += _wc(p) for ftype in files: files[ftype].sort() @@ -1163,17 +1326,47 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace: "needs_graph": needs_graph, "warning": warning, "skipped_sensitive": skipped_sensitive, + "unclassified": sorted(unclassified), + "walk_errors": walk_errors, "graphifyignore_patterns": len(ignore_patterns), "scan_root": str(root.resolve()), } +def _os_path(path: Path) -> str: + r"""Return an OS path string safe for open()/stat() on Windows long paths. + + On win32, paths longer than the legacy MAX_PATH (260 chars) are rejected by + the plain file APIs unless prefixed with the extended-length marker ``\\?\`` + (which also requires a fully-qualified path). Without it, _md5_file / + save_manifest / count_words silently fail to hash deeply-nested files, so + their manifest entry never stabilizes and detect_incremental re-flags them + as changed on every run (#1655). cache._normalize_path strips this prefix + for stable KEYS; this adds it for I/O. Non-win32 and already-prefixed paths + pass through unchanged. + """ + import sys + if sys.platform != "win32": + return str(path) + s = str(path) + if s.startswith("\\\\?\\"): + return s + try: + s = os.path.abspath(s) # \\?\ requires a fully-qualified path + except Exception: + return str(path) + if s.startswith("\\\\"): + # UNC share \\server\share -> \\?\UNC\server\share + return "\\\\?\\UNC\\" + s[2:] + return "\\\\?\\" + s + + def _md5_file(path: Path) -> str: """MD5 of file contents streamed in 64KB chunks — for change detection only.""" import hashlib as _hl h = _hl.md5(usedforsecurity=False) try: - with path.open("rb") as f: + with open(_os_path(path), "rb") as f: for chunk in iter(lambda: f.read(65536), b""): h.update(chunk) except OSError: @@ -1185,7 +1378,7 @@ def _stat_and_hash(path_str: str) -> tuple[str, float, str] | None: """Stat + MD5 a single file; returns None on OSError (e.g. deleted mid-run).""" try: p = Path(path_str) - return path_str, p.stat().st_mtime, _md5_file(p) + return path_str, os.stat(_os_path(p)).st_mtime, _md5_file(p) except OSError: return None @@ -1364,11 +1557,10 @@ def detect_incremental( Backwards compatible with legacy manifests storing plain float mtime values or {mtime, hash} dicts (treated as ast_hash only; semantic_hash = miss). - The ``follow_symlinks`` flag is forwarded to :func:`detect` so corpora that - rely on symlinked sub-trees (e.g. a ``state_of_truth/`` symlink pointing to a - directory outside the scan root) are scanned consistently between full and - incremental runs. ``None`` (default) means auto-detect: ``True`` when ``root`` - contains at least one direct symlinked child, ``False`` otherwise. + The ``follow_symlinks`` flag is forwarded to :func:`detect` so in-root + symlinked sub-trees are scanned consistently between full and incremental + runs. ``None`` (default) does not follow symlinked directories; callers must + opt in explicitly, and resolved targets outside the scan root are skipped. """ full = detect(root, follow_symlinks=follow_symlinks, google_workspace=google_workspace, extra_excludes=extra_excludes) # Pass ``root`` so a manifest written with relative keys (post-#777) is @@ -1391,7 +1583,7 @@ def detect_incremental( for f in file_list: stored = manifest.get(f) try: - current_mtime = Path(f).stat().st_mtime + current_mtime = os.stat(_os_path(Path(f))).st_mtime except Exception: current_mtime = 0 diff --git a/graphify/diagnostics.py b/graphify/diagnostics.py index 4d8abe294..ff66fa958 100644 --- a/graphify/diagnostics.py +++ b/graphify/diagnostics.py @@ -274,7 +274,13 @@ def _read_json_file(path: str | Path) -> dict[str, Any]: json_path = Path(path) check_graph_file_size_cap(json_path) - data = json.loads(json_path.read_text(encoding="utf-8")) + try: + data = json.loads(json_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError) as exc: + raise RuntimeError( + f"Cannot parse {json_path}: {exc}. " + "The file may be corrupted — re-run 'graphify extract'." + ) from exc if not isinstance(data, dict): raise ValueError("diagnostic input must be a JSON object") return data diff --git a/graphify/export.py b/graphify/export.py index 4ce2d3b4e..0e95b467f 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -17,6 +17,8 @@ from graphify.analyze import _node_community_map from graphify.build import edge_data +from graphify.exporters.graphdb import push_to_falkordb, push_to_neo4j # noqa: E402,F401 + # Artifacts worth preserving across rebuilds (non-regenerable without LLM or curation). _BACKUP_ARTIFACTS = [ @@ -149,313 +151,9 @@ def _yaml_str(s: str) -> str: return "".join(out) -COMMUNITY_COLORS = [ - "#4E79A7", "#F28E2B", "#E15759", "#76B7B2", "#59A14F", - "#EDC948", "#B07AA1", "#FF9DA7", "#9C755F", "#BAB0AC", -] - -MAX_NODES_FOR_VIZ = 5_000 - - -def _viz_node_limit() -> int: - """Return the effective viz node limit, honoring GRAPHIFY_VIZ_NODE_LIMIT env var. +from graphify.exporters.base import COMMUNITY_COLORS # noqa: E402,F401 - Falls back to MAX_NODES_FOR_VIZ when the env var is unset, empty, or non-integer. - Set to 0 to disable HTML viz unconditionally (useful for CI runners). - """ - import os - raw = os.environ.get("GRAPHIFY_VIZ_NODE_LIMIT") - if raw is None or not raw.strip(): - return MAX_NODES_FOR_VIZ - try: - return int(raw) - except ValueError: - return MAX_NODES_FOR_VIZ - - -def _html_styles() -> str: - return """""" - - -def _hyperedge_script(hyperedges_json: str) -> str: - return f"""""" - - -def _html_script(nodes_json: str, edges_json: str, legend_json: str) -> str: - return f"""""" +from graphify.exporters.html import to_html # noqa: E402,F401 _CONFIDENCE_SCORE_DEFAULTS = {"EXTRACTED": 1.0, "INFERRED": 0.5, "AMBIGUOUS": 0.2} @@ -486,11 +184,44 @@ def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, # Safety check: refuse to silently shrink an existing graph (#479) existing_path = Path(output_path) if not force and existing_path.exists(): + from graphify.security import check_graph_file_size_cap try: - from graphify.security import check_graph_file_size_cap check_graph_file_size_cap(existing_path) - existing_data = json.loads(existing_path.read_text(encoding="utf-8")) - existing_n = len(existing_data.get("nodes", [])) + except Exception: + # Existing graph.json trips the size cap; reading it to compare would + # be the very DoS the cap guards against. Can't verify — let the new + # graph replace the oversized file. + oversized = True + else: + oversized = False + if not oversized: + try: + raw = existing_path.read_text(encoding="utf-8") + except Exception: + raw = "" + if not raw.strip(): + # Empty/whitespace existing file (e.g. a freshly touched path): + # no nodes to lose, so any new graph is a growth — proceed. + existing_n = 0 + else: + try: + existing_data = json.loads(raw) + existing_n = len(existing_data.get("nodes", [])) + except Exception as exc: + # Non-empty but unparseable existing graph (corrupt or a + # mid-write): we cannot verify the new graph is not a silent + # shrink. Fail SAFE — refuse rather than overwrite. A + # fail-OPEN here (the prior behavior) is the silent data-loss + # path #479 exists to prevent: a transiently unreadable + # graph.json would let a partial rebuild clobber a good one. + import sys as _sys + print( + f"[graphify] WARNING: existing {existing_path} could not be " + f"read to verify the new graph is not smaller ({exc}). " + f"Refusing to overwrite; pass force=True to override.", + file=_sys.stderr, + ) + return False new_n = G.number_of_nodes() if new_n < existing_n: import sys as _sys @@ -505,8 +236,6 @@ def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, file=_sys.stderr, ) return False - except Exception: - pass # unreadable existing file — proceed with write node_community = _node_community_map(communities) _labels: dict[int, str] = {int(k): v for k, v in (community_labels or {}).items()} @@ -629,197 +358,6 @@ def to_cypher(G: nx.Graph, output_path: str) -> None: f.write("\n".join(lines)) -def to_html( - G: nx.Graph, - communities: dict[int, list[str]], - output_path: str, - community_labels: dict[int, str] | None = None, - member_counts: dict[int, int] | None = None, - node_limit: int | None = None, -) -> None: - """Generate an interactive vis.js HTML visualization of the graph. - - Features: node size by degree, click-to-inspect panel, search box, - community filter, physics clustering by community, confidence-styled edges. - Raises ValueError if graph exceeds MAX_NODES_FOR_VIZ. - - If member_counts is provided (aggregated community view), node sizes are - based on community member counts rather than graph degree. - - If node_limit is set and the graph exceeds it, automatically builds an - aggregated community-level meta-graph instead of raising ValueError. - """ - limit = node_limit if node_limit is not None else _viz_node_limit() - if G.number_of_nodes() > limit: - if node_limit is not None: - # Build aggregated community meta-graph - from collections import Counter as _Counter - import networkx as _nx - print(f"Graph has {G.number_of_nodes()} nodes (above {limit} limit). Building aggregated community view...") - node_to_community = {nid: cid for cid, members in communities.items() for nid in members} - meta = _nx.Graph() - for cid, members in communities.items(): - meta.add_node(str(cid), label=(community_labels or {}).get(cid, f"Community {cid}")) - edge_counts = _Counter() - for u, v in G.edges(): - cu, cv = node_to_community.get(u), node_to_community.get(v) - if cu is not None and cv is not None and cu != cv: - edge_counts[(min(cu, cv), max(cu, cv))] += 1 - for (cu, cv), w in edge_counts.items(): - meta.add_edge(str(cu), str(cv), weight=w, - relation=f"{w} cross-community edges", confidence="AGGREGATED") - if meta.number_of_nodes() <= 1: - print("Single community - aggregated view not useful. Skipping graph.html.") - return - meta_communities = {cid: [str(cid)] for cid in communities} - mc = {cid: len(members) for cid, members in communities.items()} - # Remap hyperedges from semantic node IDs to community IDs - raw_hyperedges = G.graph.get("hyperedges", []) - if raw_hyperedges: - remapped = [] - for he in raw_hyperedges: - he_members = he.get("nodes") or he.get("members") or [] - comm_ids, seen = [], set() - for nid in he_members: - c = node_to_community.get(nid) - if c is None: - continue - s = str(c) - if s in seen: - continue - seen.add(s) - comm_ids.append(s) - if len(comm_ids) < 2: - continue - remapped.append({ - "id": he.get("id", ""), - "label": he.get("label") or he.get("relation", "").replace("_", " "), - "nodes": comm_ids, - }) - meta.graph["hyperedges"] = remapped - to_html(meta, meta_communities, output_path, - community_labels=community_labels, member_counts=mc) - print(f"graph.html written (aggregated: {meta.number_of_nodes()} community nodes, {meta.number_of_edges()} cross-community edges)") - print("Tip: run with --obsidian for full node-level detail.") - return - raise ValueError( - f"Graph has {G.number_of_nodes()} nodes - too large for HTML viz " - f"(limit: {limit}). Use --no-viz, raise GRAPHIFY_VIZ_NODE_LIMIT, " - f"or reduce input size." - ) - - node_community = _node_community_map(communities) - degree = dict(G.degree()) - max_deg = max(degree.values(), default=1) or 1 - max_mc = (max(member_counts.values(), default=1) or 1) if member_counts else 1 - - # Build nodes list for vis.js - vis_nodes = [] - for node_id, data in G.nodes(data=True): - cid = node_community.get(node_id, 0) - color = COMMUNITY_COLORS[cid % len(COMMUNITY_COLORS)] - label = sanitize_label(data.get("label", node_id)) - deg = degree.get(node_id, 1) - if member_counts: - mc = member_counts.get(cid, 1) - size = 10 + 30 * (mc / max_mc) - font_size = 12 - else: - size = 10 + 30 * (deg / max_deg) - # Only show label for high-degree nodes by default; others show on hover - font_size = 12 if deg >= max_deg * 0.15 else 0 - vis_nodes.append({ - "id": node_id, - "label": label, - "color": {"background": color, "border": color, "highlight": {"background": "#ffffff", "border": color}}, - "size": round(size, 1), - "font": {"size": font_size, "color": "#ffffff"}, - "title": _html.escape(label), - "community": cid, - "community_name": sanitize_label((community_labels or {}).get(cid, f"Community {cid}")), - "source_file": sanitize_label(str(data.get("source_file") or "")), - "file_type": data.get("file_type", ""), - "degree": deg, - }) - - # Build edges list. Restore original edge direction from _src/_tgt - # (stashed by build.py for exactly this reason): undirected NetworkX - # canonicalizes endpoint order, which would otherwise flip the arrow - # for `calls` and `rationale_for` in the rendered graph (#563). - vis_edges = [] - for u, v, data in G.edges(data=True): - confidence = data.get("confidence", "EXTRACTED") - relation = data.get("relation", "") - true_src = data.get("_src", u) - true_tgt = data.get("_tgt", v) - vis_edges.append({ - "from": true_src, - "to": true_tgt, - "label": relation, - "title": _html.escape(f"{relation} [{confidence}]"), - "dashes": confidence != "EXTRACTED", - "width": 2 if confidence == "EXTRACTED" else 1, - "color": {"opacity": 0.7 if confidence == "EXTRACTED" else 0.35}, - "confidence": confidence, - }) - - # Build community legend data - legend_data = [] - for cid in sorted((community_labels or {}).keys()): - color = COMMUNITY_COLORS[cid % len(COMMUNITY_COLORS)] - lbl = _html.escape(sanitize_label((community_labels or {}).get(cid, f"Community {cid}"))) - n = member_counts.get(cid, len(communities.get(cid, []))) if member_counts else len(communities.get(cid, [])) - legend_data.append({"cid": cid, "color": color, "label": lbl, "count": n}) - - # Escape sequences so embedded JSON cannot break out of the script tag - def _js_safe(obj) -> str: - return json.dumps(obj).replace(" - - - -graphify - {title} - -{_html_styles()} - - -
- -{_html_script(nodes_json, edges_json, legend_json)} -{_hyperedge_script(hyperedges_json)} - -""" - - Path(output_path).write_text(html, encoding="utf-8") # nosec - - # Keep backward-compatible alias - skill.md calls generate_html generate_html = to_html @@ -1231,7 +769,10 @@ def safe_name(label: str) -> str: group_sizes: dict[int, tuple[int, int]] = {} group_cols: dict[int, int] = {} for cid in sorted_cids: - members = communities[cid] + # Skip dangling community members with no backing node / filename, so box + # sizing matches the cards actually laid out and `G.nodes[m]` never + # KeyErrors below — mirrors the to_obsidian guard (#1236). + members = [m for m in communities[cid] if m in G and m in node_filenames] n = len(members) inner_cols = max(1, math.ceil(math.sqrt(n))) w = max(600, 220 * inner_cols) @@ -1304,6 +845,9 @@ def safe_name(label: str) -> str: # Node cards inside the group - laid out in the same ceil(sqrt(n))-column # grid the box was sized for (group_cols[cid]), so cards fill the box. inner_cols = group_cols[cid] + # Same dangling-member guard as the sizing loop and to_obsidian (#1236): + # a community id absent from G / node_filenames would KeyError the sort. + members = [m for m in members if m in G and m in node_filenames] sorted_members = sorted(members, key=lambda n: G.nodes[n].get("label", n)) for m_idx, node_id in enumerate(sorted_members): col = m_idx % inner_cols @@ -1344,174 +888,6 @@ def safe_name(label: str) -> str: Path(output_path).write_text(json.dumps(canvas_data, indent=2), encoding="utf-8") # nosec -def push_to_neo4j( - G: nx.Graph, - uri: str, - user: str, - password: str, - communities: dict[int, list[str]] | None = None, -) -> dict[str, int]: - """Push graph directly to a running Neo4j instance via the Python driver. - - Requires: pip install neo4j - - Uses MERGE so re-running is safe - nodes and edges are upserted, not duplicated. - Returns a dict with counts of nodes and edges pushed. - """ - try: - from neo4j import GraphDatabase - except ImportError as e: - raise ImportError( - "neo4j driver not installed. Run: pip install neo4j" - ) from e - - node_community = _node_community_map(communities) if communities else {} - - def _safe_rel(relation: str) -> str: - return re.sub(r"[^A-Z0-9_]", "_", relation.upper().replace(" ", "_").replace("-", "_")) or "RELATED_TO" - - def _safe_label(label: str) -> str: - """Sanitize a Neo4j node label to prevent Cypher injection.""" - sanitized = re.sub(r"[^A-Za-z0-9_]", "", label) - return sanitized if sanitized else "Entity" - - driver = GraphDatabase.driver(uri, auth=(user, password)) - nodes_pushed = 0 - edges_pushed = 0 - - with driver.session() as session: - for node_id, data in G.nodes(data=True): - props = { - k: v for k, v in data.items() - if isinstance(v, (str, int, float, bool)) and not k.startswith("_") - } - props["id"] = node_id - cid = node_community.get(node_id) - if cid is not None: - props["community"] = cid - ftype = _safe_label(data.get("file_type", "Entity").capitalize()) - session.run( - f"MERGE (n:{ftype} {{id: $id}}) SET n += $props", - id=node_id, - props=props, - ) - nodes_pushed += 1 - - for u, v, data in G.edges(data=True): - rel = _safe_rel(data.get("relation", "RELATED_TO")) - props = { - k: v for k, v in data.items() - if isinstance(v, (str, int, float, bool)) and not k.startswith("_") - } - session.run( - f"MATCH (a {{id: $src}}), (b {{id: $tgt}}) " - f"MERGE (a)-[r:{rel}]->(b) SET r += $props", - src=u, - tgt=v, - props=props, - ) - edges_pushed += 1 - - driver.close() - return {"nodes": nodes_pushed, "edges": edges_pushed} - - -def push_to_falkordb( - G: nx.Graph, - uri: str, - user: str | None = None, - password: str | None = None, - communities: dict[int, list[str]] | None = None, - graph_name: str = "graphify", -) -> dict[str, int]: - """Push graph directly to a running FalkorDB instance via the Python SDK. - - Requires: pip install falkordb - - FalkorDB is OpenCypher-compatible, so the MERGE/SET upsert queries are - identical to push_to_neo4j. Differences from the Neo4j path: - - connects with FalkorDB(host, port, username, password) instead of a bolt - driver; only the host/port are read from the URI, so the scheme is - informational - "falkordb://localhost:6379", "redis://localhost:6379" - and a bare "localhost:6379" are all equivalent (default port 6379). - - a named graph is selected via db.select_graph(graph_name) (default - "graphify"); FalkorDB keys each graph by name in the same instance. - - queries run via graph.query(cypher, params) - there is no session object. - - auth is optional (FalkorDB runs without credentials by default), so user - and password may be None. - - no APOC: the Neo4j path does not use APOC either, so nothing to port. - - Uses MERGE so re-running is safe - nodes and edges are upserted, not - duplicated. Returns a dict with counts of nodes and edges pushed. - """ - try: - from falkordb import FalkorDB - except ImportError as e: - raise ImportError( - "falkordb SDK not installed. Run: pip install falkordb" - ) from e - - from urllib.parse import urlparse - - node_community = _node_community_map(communities) if communities else {} - - def _safe_rel(relation: str) -> str: - return re.sub(r"[^A-Z0-9_]", "_", relation.upper().replace(" ", "_").replace("-", "_")) or "RELATED_TO" - - def _safe_label(label: str) -> str: - """Sanitize a FalkorDB node label to prevent Cypher injection.""" - sanitized = re.sub(r"[^A-Za-z0-9_]", "", label) - return sanitized if sanitized else "Entity" - - parsed = urlparse(uri if "://" in uri else f"redis://{uri}") - # FalkorDB auth is optional. Only send credentials when a password is - # provided; otherwise connect anonymously and ignore any bolt-style default - # username (e.g. Neo4j's "neo4j"), which FalkorDB rejects as an unknown ACL - # user. Credentials embedded in the URI take precedence over the args. - connect_user = parsed.username or (user if password else None) - connect_password = parsed.password or (password or None) - db = FalkorDB( - host=parsed.hostname or "localhost", - port=parsed.port or 6379, - username=connect_user, - **{"password": connect_password}, - ) - graph = db.select_graph(graph_name) - nodes_pushed = 0 - edges_pushed = 0 - - for node_id, data in G.nodes(data=True): - props = { - k: v for k, v in data.items() - if isinstance(v, (str, int, float, bool)) and not k.startswith("_") - } - props["id"] = node_id - cid = node_community.get(node_id) - if cid is not None: - props["community"] = cid - ftype = _safe_label(data.get("file_type", "Entity").capitalize()) - graph.query( - f"MERGE (n:{ftype} {{id: $id}}) SET n += $props", - {"id": node_id, "props": props}, - ) - nodes_pushed += 1 - - for u, v, data in G.edges(data=True): - rel = _safe_rel(data.get("relation", "RELATED_TO")) - props = { - k: v for k, v in data.items() - if isinstance(v, (str, int, float, bool)) and not k.startswith("_") - } - graph.query( - f"MATCH (a {{id: $src}}), (b {{id: $tgt}}) " - f"MERGE (a)-[r:{rel}]->(b) SET r += $props", - {"src": u, "tgt": v, "props": props}, - ) - edges_pushed += 1 - - return {"nodes": nodes_pushed, "edges": edges_pushed} - - def to_graphml( G: nx.Graph, communities: dict[int, list[str]], @@ -1535,16 +911,44 @@ def to_graphml( for _, _, attrs in H.edges(data=True): for k in [k for k in attrs if k.startswith("_")]: del attrs[k] - # nx.write_graphml raises ValueError on None attribute values; replace with "". + # nx.write_graphml only accepts scalar attribute values: None raises, and a + # dict/list value (e.g. a per-node `metadata` dict, or the graph-level + # `hyperedges` list set by attach_hyperedges()) raises + # "GraphML does not support type as data values" (#1831). + # Coerce None -> "" and non-scalars -> a JSON string, across all three scopes. + def _graphml_safe(val): + if val is None: + return "" + if isinstance(val, bool) or isinstance(val, (int, float, str)): + return val # GraphML-native scalars pass through unchanged + try: + return json.dumps(val, default=str, sort_keys=True) + except (TypeError, ValueError): + return str(val) + + for key, val in list(H.graph.items()): + H.graph[key] = _graphml_safe(val) for node_id in H.nodes(): for key, val in list(H.nodes[node_id].items()): - if val is None: - H.nodes[node_id][key] = "" + H.nodes[node_id][key] = _graphml_safe(val) for u, v in H.edges(): for key, val in list(H.edges[u, v].items()): - if val is None: - H.edges[u, v][key] = "" - nx.write_graphml(H, output_path) + H.edges[u, v][key] = _graphml_safe(val) + + # Write atomically: a mid-serialization error otherwise leaves a 0-byte + # .graphml on disk that downstream tooling mistakes for a completed export + # (#1831). Write to a sibling temp file, then replace on success. + out = Path(output_path) + tmp = out.with_name(out.name + ".tmp") + try: + nx.write_graphml(H, str(tmp)) + os.replace(str(tmp), str(out)) + finally: + if tmp.exists(): + try: + tmp.unlink() + except OSError: + pass def to_svg( diff --git a/graphify/exporters/__init__.py b/graphify/exporters/__init__.py new file mode 100644 index 000000000..41e1aeff9 --- /dev/null +++ b/graphify/exporters/__init__.py @@ -0,0 +1 @@ +"""exporters package.""" diff --git a/graphify/exporters/base.py b/graphify/exporters/base.py new file mode 100644 index 000000000..9c9419646 --- /dev/null +++ b/graphify/exporters/base.py @@ -0,0 +1,14 @@ +"""Shared constants/helpers for the graphify exporters package. + +Symbols used by more than one exporter live here so each exporter module can be +split out of graphify/export.py without a circular import (export.py and the +per-format modules both import from here, never from each other). +""" +from __future__ import annotations + +# Categorical palette for community coloring, shared by the HTML, SVG, and +# Obsidian exporters. Moved verbatim from graphify/export.py. +COMMUNITY_COLORS = [ + "#4E79A7", "#F28E2B", "#E15759", "#76B7B2", "#59A14F", + "#EDC948", "#B07AA1", "#FF9DA7", "#9C755F", "#BAB0AC", +] diff --git a/graphify/exporters/graphdb.py b/graphify/exporters/graphdb.py new file mode 100644 index 000000000..b67c5dd10 --- /dev/null +++ b/graphify/exporters/graphdb.py @@ -0,0 +1,173 @@ +"""graphdb — moved verbatim from graphify/export.py.""" +from __future__ import annotations + +from graphify.analyze import _node_community_map +import networkx as nx +import re + + +def push_to_neo4j( + G: nx.Graph, + uri: str, + user: str, + password: str, + communities: dict[int, list[str]] | None = None, +) -> dict[str, int]: + """Push graph directly to a running Neo4j instance via the Python driver. + + Requires: pip install neo4j + + Uses MERGE so re-running is safe - nodes and edges are upserted, not duplicated. + Returns a dict with counts of nodes and edges pushed. + """ + try: + from neo4j import GraphDatabase + except ImportError as e: + raise ImportError( + "neo4j driver not installed. Run: pip install neo4j" + ) from e + + node_community = _node_community_map(communities) if communities else {} + + def _safe_rel(relation: str) -> str: + return re.sub(r"[^A-Z0-9_]", "_", relation.upper().replace(" ", "_").replace("-", "_")) or "RELATED_TO" + + def _safe_label(label: str) -> str: + """Sanitize a Neo4j node label to prevent Cypher injection.""" + sanitized = re.sub(r"[^A-Za-z0-9_]", "", label) + return sanitized if sanitized else "Entity" + + driver = GraphDatabase.driver(uri, auth=(user, password)) + nodes_pushed = 0 + edges_pushed = 0 + + with driver.session() as session: + for node_id, data in G.nodes(data=True): + props = { + k: v for k, v in data.items() + if isinstance(v, (str, int, float, bool)) and not k.startswith("_") + } + props["id"] = node_id + cid = node_community.get(node_id) + if cid is not None: + props["community"] = cid + ftype = _safe_label(data.get("file_type", "Entity").capitalize()) + session.run( + f"MERGE (n:{ftype} {{id: $id}}) SET n += $props", + id=node_id, + props=props, + ) + nodes_pushed += 1 + + for u, v, data in G.edges(data=True): + rel = _safe_rel(data.get("relation", "RELATED_TO")) + props = { + k: v for k, v in data.items() + if isinstance(v, (str, int, float, bool)) and not k.startswith("_") + } + session.run( + f"MATCH (a {{id: $src}}), (b {{id: $tgt}}) " + f"MERGE (a)-[r:{rel}]->(b) SET r += $props", + src=u, + tgt=v, + props=props, + ) + edges_pushed += 1 + + driver.close() + return {"nodes": nodes_pushed, "edges": edges_pushed} + +def push_to_falkordb( + G: nx.Graph, + uri: str, + user: str | None = None, + password: str | None = None, + communities: dict[int, list[str]] | None = None, + graph_name: str = "graphify", +) -> dict[str, int]: + """Push graph directly to a running FalkorDB instance via the Python SDK. + + Requires: pip install falkordb + + FalkorDB is OpenCypher-compatible, so the MERGE/SET upsert queries are + identical to push_to_neo4j. Differences from the Neo4j path: + - connects with FalkorDB(host, port, username, password) instead of a bolt + driver; only the host/port are read from the URI, so the scheme is + informational - "falkordb://localhost:6379", "redis://localhost:6379" + and a bare "localhost:6379" are all equivalent (default port 6379). + - a named graph is selected via db.select_graph(graph_name) (default + "graphify"); FalkorDB keys each graph by name in the same instance. + - queries run via graph.query(cypher, params) - there is no session object. + - auth is optional (FalkorDB runs without credentials by default), so user + and password may be None. + - no APOC: the Neo4j path does not use APOC either, so nothing to port. + + Uses MERGE so re-running is safe - nodes and edges are upserted, not + duplicated. Returns a dict with counts of nodes and edges pushed. + """ + try: + from falkordb import FalkorDB + except ImportError as e: + raise ImportError( + "falkordb SDK not installed. Run: pip install falkordb" + ) from e + + from urllib.parse import urlparse + + node_community = _node_community_map(communities) if communities else {} + + def _safe_rel(relation: str) -> str: + return re.sub(r"[^A-Z0-9_]", "_", relation.upper().replace(" ", "_").replace("-", "_")) or "RELATED_TO" + + def _safe_label(label: str) -> str: + """Sanitize a FalkorDB node label to prevent Cypher injection.""" + sanitized = re.sub(r"[^A-Za-z0-9_]", "", label) + return sanitized if sanitized else "Entity" + + parsed = urlparse(uri if "://" in uri else f"redis://{uri}") + # FalkorDB auth is optional. Only send credentials when a password is + # provided; otherwise connect anonymously and ignore any bolt-style default + # username (e.g. Neo4j's "neo4j"), which FalkorDB rejects as an unknown ACL + # user. Credentials embedded in the URI take precedence over the args. + connect_user = parsed.username or (user if password else None) + connect_password = parsed.password or (password or None) + db = FalkorDB( + host=parsed.hostname or "localhost", + port=parsed.port or 6379, + username=connect_user, + **{"password": connect_password}, + ) + graph = db.select_graph(graph_name) + nodes_pushed = 0 + edges_pushed = 0 + + for node_id, data in G.nodes(data=True): + props = { + k: v for k, v in data.items() + if isinstance(v, (str, int, float, bool)) and not k.startswith("_") + } + props["id"] = node_id + cid = node_community.get(node_id) + if cid is not None: + props["community"] = cid + ftype = _safe_label(data.get("file_type", "Entity").capitalize()) + graph.query( + f"MERGE (n:{ftype} {{id: $id}}) SET n += $props", + {"id": node_id, "props": props}, + ) + nodes_pushed += 1 + + for u, v, data in G.edges(data=True): + rel = _safe_rel(data.get("relation", "RELATED_TO")) + props = { + k: v for k, v in data.items() + if isinstance(v, (str, int, float, bool)) and not k.startswith("_") + } + graph.query( + f"MATCH (a {{id: $src}}), (b {{id: $tgt}}) " + f"MERGE (a)-[r:{rel}]->(b) SET r += $props", + {"src": u, "tgt": v, "props": props}, + ) + edges_pushed += 1 + + return {"nodes": nodes_pushed, "edges": edges_pushed} diff --git a/graphify/exporters/html.py b/graphify/exporters/html.py new file mode 100644 index 000000000..5ba61ce24 --- /dev/null +++ b/graphify/exporters/html.py @@ -0,0 +1,547 @@ +"""html — moved verbatim from graphify/export.py.""" +from __future__ import annotations + +from graphify.exporters.base import COMMUNITY_COLORS # noqa: E402,F401 +from pathlib import Path +import html as _html +from graphify.analyze import _node_community_map +import json +import networkx as nx +from graphify.security import sanitize_label + + +MAX_NODES_FOR_VIZ = 5_000 + +def _viz_node_limit() -> int: + """Return the effective viz node limit, honoring GRAPHIFY_VIZ_NODE_LIMIT env var. + + Falls back to MAX_NODES_FOR_VIZ when the env var is unset, empty, or non-integer. + Set to 0 to disable HTML viz unconditionally (useful for CI runners). + """ + import os + raw = os.environ.get("GRAPHIFY_VIZ_NODE_LIMIT") + if raw is None or not raw.strip(): + return MAX_NODES_FOR_VIZ + try: + return int(raw) + except ValueError: + return MAX_NODES_FOR_VIZ + +def _html_styles() -> str: + return """""" + +def _hyperedge_script(hyperedges_json: str) -> str: + return f"""""" + +def _html_script(nodes_json: str, edges_json: str, legend_json: str) -> str: + return f"""""" + +def to_html( + G: nx.Graph, + communities: dict[int, list[str]], + output_path: str, + community_labels: dict[int, str] | None = None, + member_counts: dict[int, int] | None = None, + node_limit: int | None = None, + learning_overlay: dict | None = None, +) -> None: + """Generate an interactive vis.js HTML visualization of the graph. + + Features: node size by degree, click-to-inspect panel, search box, + community filter, physics clustering by community, confidence-styled edges. + Raises ValueError if graph exceeds MAX_NODES_FOR_VIZ. + + If member_counts is provided (aggregated community view), node sizes are + based on community member counts rather than graph degree. + + If node_limit is set and the graph exceeds it, automatically builds an + aggregated community-level meta-graph instead of raising ValueError. + """ + limit = node_limit if node_limit is not None else _viz_node_limit() + if G.number_of_nodes() > limit: + if node_limit is not None: + # Build aggregated community meta-graph + from collections import Counter as _Counter + import networkx as _nx + print(f"Graph has {G.number_of_nodes()} nodes (above {limit} limit). Building aggregated community view...") + node_to_community = {nid: cid for cid, members in communities.items() for nid in members} + meta = _nx.Graph() + for cid, members in communities.items(): + meta.add_node(str(cid), label=(community_labels or {}).get(cid, f"Community {cid}")) + edge_counts = _Counter() + for u, v in G.edges(): + cu, cv = node_to_community.get(u), node_to_community.get(v) + if cu is not None and cv is not None and cu != cv: + edge_counts[(min(cu, cv), max(cu, cv))] += 1 + for (cu, cv), w in edge_counts.items(): + meta.add_edge(str(cu), str(cv), weight=w, + relation=f"{w} cross-community edges", confidence="AGGREGATED") + if meta.number_of_nodes() <= 1: + print("Single community - aggregated view not useful. Skipping graph.html.") + return + meta_communities = {cid: [str(cid)] for cid in communities} + mc = {cid: len(members) for cid, members in communities.items()} + # Remap hyperedges from semantic node IDs to community IDs + raw_hyperedges = G.graph.get("hyperedges", []) + if raw_hyperedges: + remapped = [] + for he in raw_hyperedges: + he_members = he.get("nodes", []) + comm_ids, seen = [], set() + for nid in he_members: + c = node_to_community.get(nid) + if c is None: + continue + s = str(c) + if s in seen: + continue + seen.add(s) + comm_ids.append(s) + if len(comm_ids) < 2: + continue + remapped.append({ + "id": he.get("id", ""), + "label": he.get("label") or he.get("relation", "").replace("_", " "), + "nodes": comm_ids, + }) + meta.graph["hyperedges"] = remapped + to_html(meta, meta_communities, output_path, + community_labels=community_labels, member_counts=mc) + print(f"graph.html written (aggregated: {meta.number_of_nodes()} community nodes, {meta.number_of_edges()} cross-community edges)") + print("Tip: run with --obsidian for full node-level detail.") + return + raise ValueError( + f"Graph has {G.number_of_nodes()} nodes - too large for HTML viz " + f"(limit: {limit}). Use --no-viz, raise GRAPHIFY_VIZ_NODE_LIMIT, " + f"or reduce input size." + ) + + node_community = _node_community_map(communities) + degree = dict(G.degree()) + max_deg = max(degree.values(), default=1) or 1 + max_mc = (max(member_counts.values(), default=1) or 1) if member_counts else 1 + + # Work-memory overlay (derived sidecar). When not passed explicitly, load it + # best-effort from the sibling .graphify_learning.json next to the output + # graph.html (which lives beside graph.json). Empty/missing => no learning + # fields, so the un-annotated render is byte-identical to pre-feature. + if learning_overlay is None: + learning_overlay = {} + try: + from graphify.reflect import load_learning_overlay as _llo + learning_overlay = _llo(Path(output_path)) + except Exception: + learning_overlay = {} + # Status -> ring color. preferred=green, contested=amber. Tentative gets no + # ring (it's not yet trustworthy enough to highlight in the map). + _RING = {"preferred": "#22c55e", "contested": "#f59e0b"} + + # Build nodes list for vis.js + vis_nodes = [] + for node_id, data in G.nodes(data=True): + cid = node_community.get(node_id, 0) + color = COMMUNITY_COLORS[cid % len(COMMUNITY_COLORS)] + label = sanitize_label(data.get("label", node_id)) + deg = degree.get(node_id, 1) + if member_counts: + mc = member_counts.get(cid, 1) + size = 10 + 30 * (mc / max_mc) + font_size = 12 + else: + size = 10 + 30 * (deg / max_deg) + # Only show label for high-degree nodes by default; others show on hover + font_size = 12 if deg >= max_deg * 0.15 else 0 + node = { + "id": node_id, + "label": label, + "color": {"background": color, "border": color, "highlight": {"background": "#ffffff", "border": color}}, + "size": round(size, 1), + "font": {"size": font_size, "color": "#ffffff"}, + "title": _html.escape(label), + "community": cid, + "community_name": sanitize_label((community_labels or {}).get(cid, f"Community {cid}")), + "source_file": sanitize_label(str(data.get("source_file") or "")), + "file_type": data.get("file_type", ""), + "degree": deg, + } + # Conditional learning fields — only present for annotated nodes, so + # un-annotated output keeps the exact pre-feature node dict shape. + entry = learning_overlay.get(str(node_id)) if learning_overlay else None + if entry: + status = sanitize_label(str(entry.get("status", ""))) + stale = bool(entry.get("stale")) + node["learning_status"] = status + node["learning_stale"] = stale + ring = _RING.get(status) + if ring: + # Status-colored ring via the border; stale => desaturated + + # dashed (vis.js supports per-node `shapeProperties.borderDashes`). + if stale: + ring = "#9ca3af" + node["shapeProperties"] = {"borderDashes": [4, 4]} + node["borderWidth"] = 3 + node["color"] = { + "background": color, "border": ring, + "highlight": {"background": "#ffffff", "border": ring}, + } + # Lesson line appended to the hover title. + if status == "contested": + lesson = f"Lesson: contested (useful {entry.get('uses', 0)} / dead-end {entry.get('neg', 0)})" + elif status == "preferred": + lesson = f"Lesson: preferred source ({entry.get('uses', 0)} useful, score={entry.get('score', 0)})" + else: + lesson = f"Lesson: {status} ({entry.get('uses', 0)} useful)" + if stale: + lesson += " [code changed — re-verify]" + node["title"] = _html.escape(label) + "\n" + _html.escape(sanitize_label(lesson)) + vis_nodes.append(node) + + # Build edges list. Restore original edge direction from _src/_tgt + # (stashed by build.py for exactly this reason): undirected NetworkX + # canonicalizes endpoint order, which would otherwise flip the arrow + # for `calls` and `rationale_for` in the rendered graph (#563). + vis_edges = [] + for u, v, data in G.edges(data=True): + confidence = data.get("confidence", "EXTRACTED") + relation = data.get("relation", "") + true_src = data.get("_src", u) + true_tgt = data.get("_tgt", v) + vis_edges.append({ + "from": true_src, + "to": true_tgt, + "label": relation, + "title": _html.escape(f"{relation} [{confidence}]"), + "dashes": confidence != "EXTRACTED", + "width": 2 if confidence == "EXTRACTED" else 1, + "color": {"opacity": 0.7 if confidence == "EXTRACTED" else 0.35}, + "confidence": confidence, + }) + + # Build community legend data + legend_data = [] + for cid in sorted((community_labels or {}).keys()): + color = COMMUNITY_COLORS[cid % len(COMMUNITY_COLORS)] + lbl = _html.escape(sanitize_label((community_labels or {}).get(cid, f"Community {cid}"))) + n = member_counts.get(cid, len(communities.get(cid, []))) if member_counts else len(communities.get(cid, [])) + legend_data.append({"cid": cid, "color": color, "label": lbl, "count": n}) + + # Escape sequences so embedded JSON cannot break out of the script tag + def _js_safe(obj) -> str: + return json.dumps(obj).replace(" + + + +graphify - {title} + +{_html_styles()} + + +
+ +{_html_script(nodes_json, edges_json, legend_json)} +{_hyperedge_script(hyperedges_json)} + +""" + + Path(output_path).write_text(html, encoding="utf-8") # nosec diff --git a/graphify/extract.py b/graphify/extract.py index a6f2bcd19..9571bdb53 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -20,6 +20,7 @@ run_language_resolvers, ) from .ruby_resolution import resolve_ruby_member_calls +from .pascal_resolution import resolve_pascal_inherited_calls # --- migrated to graphify/extractors/ (see graphify/extractors/MIGRATION.md) --- from graphify.extractors.base import ( # noqa: F401 @@ -28,11 +29,115 @@ _make_id, _read_text, ) +from graphify.extractors.apex import extract_apex # noqa: F401 +from graphify.extractors.bash import extract_bash # noqa: F401 from graphify.extractors.blade import extract_blade # noqa: F401 -from graphify.extractors.csharp import _resolve_csharp_type_references +from graphify.extractors.csharp import ( + _resolve_cross_file_csharp_imports, + _resolve_csharp_type_references, +) +from graphify.extractors.dart import extract_dart # noqa: F401 +from graphify.extractors.dm import extract_dm, extract_dmf, extract_dmi, extract_dmm # noqa: F401 from graphify.extractors.elixir import extract_elixir # noqa: F401 +from graphify.extractors.fortran import _cpp_preprocess, extract_fortran # noqa: F401 +from graphify.extractors.go import extract_go # noqa: F401 +from graphify.extractors.json_config import extract_json # noqa: F401 +from graphify.extractors.markdown import extract_markdown # noqa: F401 +from graphify.extractors.pascal_forms import extract_delphi_form, extract_lazarus_form # noqa: F401 +from graphify.extractors.powershell import extract_powershell, extract_powershell_manifest # noqa: F401 from graphify.extractors.razor import extract_razor # noqa: F401 +from graphify.extractors.rust import extract_rust # noqa: F401 +from graphify.extractors.sln import extract_sln # noqa: F401 +from graphify.extractors.sql import extract_sql # noqa: F401 +from graphify.extractors.terraform import extract_terraform # noqa: F401 +from graphify.extractors.verilog import extract_verilog # noqa: F401 from graphify.extractors.zig import extract_zig # noqa: F401 +from graphify.security import sanitize_metadata +from graphify.paths import disambiguate_ambiguous_candidates + +from graphify.extractors.models import LanguageConfig, _JS_CACHE_BYPASS_SUFFIXES, _NamespaceExportFact, _StarExportFact, _SymbolAliasFact, _SymbolDeclarationFact, _SymbolExportFact, _SymbolImportFact, _SymbolResolutionFacts, _SymbolUseFact, _WORKSPACE_PACKAGE_CACHE # noqa: E402,F401 + +from graphify.extractors.resolution import ( # noqa: E402,F401 + _DECLDEF_HEADER_SUFFIXES, + _DECLDEF_IMPL_SUFFIXES, + _EXPORT_CONDITION_PRIORITY, + _JS_INDEX_FILES, + _JS_PRIMITIVE_TYPES, + _JS_RESOLVE_EXTS, + _TSCONFIG_ALIAS_CACHE, + _VUE_SCRIPT_LANG_RE, + _VUE_SCRIPT_RE, + _WORKSPACE_MANIFEST_NAMES, + _apply_symbol_resolution_facts, + _augment_symbol_resolution_edges, + _collect_js_symbol_resolution_facts, + _collect_python_symbol_resolution_facts, + _contained_in_package, + _decldef_class_stem, + _disambiguate_colliding_node_ids, + _find_workspace_root, + _is_type_like_definition, + _js_call_identifier, + _js_default_export_name, + _js_default_import_name, + _js_export_clause, + _js_export_statement_is_star, + _js_exported_declaration_names, + _js_lexical_aliases, + _js_module_specifier, + _js_named_specifiers, + _js_namespace_export_name, + _js_source_path, + _js_top_level_function_bodies, + _load_tsconfig_aliases, + _load_workspace_packages, + _match_tsconfig_alias, + _merge_decl_def_classes, + _node_disambiguation_source_key, + _package_entry_candidates, + _parse_js_tree, + _parse_python_tree, + _pascal_class_stem_cache, + _pascal_project_root, + _pascal_resolve_class, + _pascal_resolve_unit, + _pascal_unit_cache, + _pnpm_workspace_globs, + _python_call_identifier, + _python_import_from_module, + _python_imported_names, + _python_top_level_function_bodies, + _read_tsconfig_aliases, + _resolve_c_include_path, + _resolve_cross_file_imports, + _resolve_cross_file_java_imports, + _resolve_export_target, + _resolve_java_type_references, + _resolve_js_import_path, + _resolve_js_import_target, + _resolve_js_module_path, + _resolve_lua_import_target, + _resolve_python_module_path, + _resolve_tsconfig_alias, + _resolve_workspace_import, + _source_key, + _strip_jsonc, + _ts_collect_type_refs, + _ts_heritage_clause_entries, + _ts_walk_class_members, + _vue_mask_non_script, + _walk_js_tree, + _walk_python_tree, + _workspace_globs, +) + +from graphify.extractors.engine import REFERENCE_CONTEXTS, _CSHARP_TYPE_PARAMETER_SCOPE_DECLARATIONS, _C_PRIMITIVE_TYPE_NODES, _JAVA_BUILTIN_TYPES, _JAVA_TYPE_PARAMETER_SCOPE_DECLARATIONS, _JS_FUNCTION_VALUE_TYPES, _JS_SCOPE_BOUNDARY, _PYTHON_ANNOTATION_NOISE, _PYTHON_TYPE_CONTAINERS, _RUBY_CLASS_FACTORIES, _c_collect_type_refs, _cpp_collect_type_refs, _cpp_declarator_name, _cpp_local_var_types, _csharp_attribute_names, _csharp_classify_base, _csharp_collect_type_refs, _csharp_extra_walk, _csharp_member_type_table, _csharp_namespace_id, _csharp_namespace_name, _csharp_pre_scan_interfaces, _csharp_type_parameters_in_scope, _dynamic_import_js, _extract_generic, _find_body, _find_require_call, _get_cpp_func_name, _java_annotation_names, _java_collect_type_refs, _java_extra_walk, _java_type_parameters_in_scope, _js_collect_pattern_idents, _js_dispatch_value_idents, _js_extra_walk, _js_local_bound_names, _js_member_assignment_target, _js_module_bound_names, _kotlin_collect_type_refs, _kotlin_function_return_type_node, _kotlin_property_type_node, _kotlin_user_type_name, _php_collect_type_refs, _php_method_return_type_node, _php_name_text, _python_collect_assignment_targets, _python_collect_param_refs, _python_collect_type_refs, _python_local_bound_names, _python_module_bound_names, _python_param_names, _read_csharp_type_name, _require_imports_js, _ruby_const_last_name, _ruby_extra_walk, _ruby_local_class_bindings, _ruby_new_class_name, _scala_collect_type_refs, _semantic_reference_edge, _source_location, _swift_classify_base, _swift_collect_type_refs, _swift_constructor_type, _swift_declaration_keyword, _swift_extra_walk, _swift_local_var_types, _swift_pre_scan, _swift_property_name, _swift_property_type_node, _swift_receiver_name, _swift_user_type_name, _ts_decorator_name, _ts_descendant_decorators, _ts_emit_decorator_edges, _ts_extra_walk, _ts_method_name, _ts_receiver_type_table # noqa: E402,F401 + +from graphify.extractors.pascal import _PAS_BEGIN_END_TOKEN_RE, _PAS_CALL_RE, _PAS_END_SEMI_RE, _PAS_IMPL_HEADER_RE, _PAS_KEYWORDS, _PAS_METHOD_DECL_RE, _PAS_MODULE_RE, _PAS_TOKEN_RE, _PAS_TYPE_HEADER_RE, _PAS_USES_RE, _extract_pascal_regex, _pascal_find_body, _pascal_split_bases, _pascal_split_sections, _pascal_split_uses, _pascal_strip_comments, extract_pascal # noqa: E402,F401 + +from graphify.extractors.objc import _objc_local_var_types, extract_objc # noqa: E402,F401 + +from graphify.extractors.julia import extract_julia # noqa: E402,F401 _RECURSION_LIMIT = 10_000 @@ -62,10 +167,6 @@ def _safe_extract(extractor: Callable, path: Path) -> dict: return {"nodes": [], "edges": [], "error": f"{type(e).__name__}: {e}"} - - - - def _file_node_id(rel_path: Path) -> str: """File-level node ID matching the skill.md spec: ``{parent_dir}_{stem}`` — one parent directory level, no extension. ``rel_path`` MUST be relative to @@ -76,1391 +177,135 @@ def _file_node_id(rel_path: Path) -> str: return _make_id(_file_stem(rel_path)) -_TSCONFIG_ALIAS_CACHE: dict[str, dict[str, list[str]]] = {} -_WORKSPACE_PACKAGE_CACHE: dict[str, dict[str, Path]] = {} -_WORKSPACE_MANIFEST_NAMES = ("pnpm-workspace.yaml", "package.json") -_JS_CACHE_BYPASS_SUFFIXES = {".js", ".jsx", ".mjs", ".ts", ".tsx", ".vue", ".svelte"} -_JS_RESOLVE_EXTS = (".ts", ".tsx", ".svelte", ".js", ".jsx", ".mjs") -_JS_INDEX_FILES = ("index.ts", "index.tsx", "index.svelte", "index.js", "index.jsx", "index.mjs") - - SEMANTIC_RELATIONS = frozenset({ "inherits", "implements", "mixes_in", "embeds", "references", "calls", "imports", "imports_from", "re_exports", "contains", "method", }) -REFERENCE_CONTEXTS = frozenset({ - "field", "parameter_type", "return_type", "generic_arg", "attribute", "value", "type", -}) - - -def _source_location(line: int | str | None) -> str | None: - if line is None: - return None - if isinstance(line, str): - return line if line.startswith("L") else f"L{line}" - return f"L{line}" - - -def _semantic_reference_edge( - source: str, - target: str, - context: str, - source_file: str, - line: int | str | None, -) -> dict: - if context not in REFERENCE_CONTEXTS: - raise ValueError(f"unknown reference context: {context}") - return { - "source": source, - "target": target, - "relation": "references", - "context": context, - "confidence": "EXTRACTED", - "source_file": source_file, - "source_location": _source_location(line), - "weight": 1.0, - } - - -def _resolve_js_import_path(candidate: Path) -> Path: - """Resolve a JS/TS/Svelte import target to a local file when it exists.""" - candidate = Path(os.path.normpath(candidate)) - if candidate.is_file(): - return candidate - - # TS ESM convention: imports often spell .js/.jsx while source is .ts/.tsx. - if candidate.suffix == ".js": - ts_candidate = candidate.with_suffix(".ts") - if ts_candidate.is_file(): - return ts_candidate - elif candidate.suffix == ".jsx": - tsx_candidate = candidate.with_suffix(".tsx") - if tsx_candidate.is_file(): - return tsx_candidate - - # Append extensions to the full filename, which covers extensionless imports, - # multi-dot helpers, and Svelte 5 rune files like Foo.svelte.ts. - for ext in _JS_RESOLVE_EXTS: - with_ext = candidate.parent / f"{candidate.name}{ext}" - if with_ext.is_file(): - return with_ext - - # Only fall back to directory indexes after file candidates lose. - if candidate.is_dir(): - for index_name in _JS_INDEX_FILES: - index_candidate = candidate / index_name - if index_candidate.is_file(): - return index_candidate - - return candidate - - -def _strip_jsonc(text: str) -> str: - """Strip // line comments, /* */ block comments, and trailing commas from JSONC. - - Preserves string contents (including // and /* inside strings) by skipping over - quoted spans first. Required for tsconfig.json files generated by SvelteKit, - NestJS, Vite, T3, Astro, etc., which use JSONC by default (#700). - """ - # Remove block and line comments while leaving string literals untouched. - pattern = re.compile( - r'"(?:\\.|[^"\\])*"' # double-quoted string (with escapes) - r"|/\*.*?\*/" # /* block comment */ - r"|//[^\n]*", # // line comment - re.DOTALL, - ) - - def _replace(match: re.Match) -> str: - matched_text = match.group(0) - if matched_text.startswith('"'): - return matched_text - return "" - - stripped = pattern.sub(_replace, text) - # Remove trailing commas before } or ] (allowing whitespace between). - stripped = re.sub(r",(\s*[}\]])", r"\1", stripped) - return stripped - - -def _read_tsconfig_aliases(tsconfig: Path, base_dir: Path, seen: set) -> dict[str, list[str]]: - """Recursively read path aliases from a tsconfig, following extends chains. - - Child config paths override parent. Circular extends are detected via seen set. - npm package configs (e.g. @tsconfig/svelte) are skipped since they're not on disk. - Handles JSONC (comments + trailing commas) which is the default tsconfig format - for SvelteKit, NestJS, Vite, T3, Astro, etc. (#700). - """ - if str(tsconfig) in seen: - return {} - seen.add(str(tsconfig)) - try: - raw = tsconfig.read_text(encoding="utf-8") - except Exception as e: - print(f" warning: could not read {tsconfig} ({type(e).__name__}: {e})", file=sys.stderr, flush=True) - return {} - try: - data = json.loads(raw) - except json.JSONDecodeError: - try: - data = json.loads(_strip_jsonc(raw)) - except json.JSONDecodeError as e: - print(f" warning: failed to parse {tsconfig} as JSON/JSONC ({e.msg} at line {e.lineno} col {e.colno})", file=sys.stderr, flush=True) - return {} - except Exception as e: - print(f" warning: failed to parse {tsconfig} ({type(e).__name__}: {e})", file=sys.stderr, flush=True) - return {} - - aliases: dict[str, list[str]] = {} - # `extends` may be a string or, since TypeScript 5.0, an array of paths. - # For an array, parents are processed in order with later entries - # overriding earlier ones; the extending config (paths below) overrides - # all parents. Without the list branch, an array `extends` raised - # `AttributeError: 'list' object has no attribute 'startswith'`, which - # _safe_extract turned into a skip of the whole file. - extends = data.get("extends") - if isinstance(extends, str): - extends_list = [extends] - elif isinstance(extends, list): - extends_list = [e for e in extends if isinstance(e, str)] - else: - extends_list = [] - for ext in extends_list: - # Skip scoped npm package configs (e.g. @tsconfig/svelte) — not on disk. - if not ext or ext.startswith("@"): - continue - extended_path = (base_dir / ext).resolve() - if not extended_path.suffix: - extended_path = extended_path.with_suffix(".json") - if extended_path.exists(): - aliases.update(_read_tsconfig_aliases(extended_path, extended_path.parent, seen)) - - # tsconfig `paths` are resolved relative to `baseUrl` (itself relative to - # the tsconfig's directory), not the tsconfig directory directly. Honoring - # baseUrl is required for the common monorepo / NestJS layout where - # baseUrl points at a subdirectory, e.g. baseUrl "./src" with - # "@services/*": ["services/*"] must resolve to /src/services rather - # than /services. Defaults to "." so configs without baseUrl (paths - # relative to the tsconfig dir, the TS 4.1+ behavior) keep working. - compiler_options = data.get("compilerOptions", {}) - base_url = compiler_options.get("baseUrl") or "." - paths_base = base_dir / base_url - paths = compiler_options.get("paths", {}) - for alias, targets in paths.items(): - if not targets: - continue - alias_prefix = alias.rstrip("/*") - # Keep ALL targets in declared order — tsc tries each until one resolves - # on disk. Discarding the fallbacks (#1531) misresolved/dropped imports - # whose file lived at a non-first target. Empty/non-string entries skipped. - target_bases = [ - str(os.path.normpath(paths_base / t.rstrip("/*"))) - for t in targets - if isinstance(t, str) and t - ] - if target_bases: - aliases[alias_prefix] = target_bases - - return aliases - - -def _load_tsconfig_aliases(start_dir: Path) -> dict[str, list[str]]: - """Walk up from start_dir to find tsconfig.json and return compilerOptions.paths aliases. - - Follows extends chains so SvelteKit/Nuxt/NestJS inherited aliases are included. - Returns a dict mapping alias prefix (e.g. "@") to an ordered list of resolved - base dirs (e.g. ["src"]) — tsc tries each in declared order (#1531). - Result is cached by tsconfig path string. - """ - current = start_dir.resolve() - for candidate in [current, *current.parents]: - tsconfig = candidate / "tsconfig.json" - if tsconfig.exists(): - key = str(tsconfig) - if key not in _TSCONFIG_ALIAS_CACHE: - _TSCONFIG_ALIAS_CACHE[key] = _read_tsconfig_aliases(tsconfig, candidate, seen=set()) - return _TSCONFIG_ALIAS_CACHE[key] - return {} - - -def _resolve_tsconfig_alias(raw: str, aliases: dict[str, list[str]]) -> "Path | None": - """Resolve `raw` against tsconfig path aliases. Try each target in declared - order; return the first whose candidate resolves to a real file (tsc parity). - If none exist, return the first candidate (no false edge fabricated, prior - single-target behavior preserved). Returns a Path or None if no alias matches.""" - for alias_prefix, alias_bases in aliases.items(): - if raw == alias_prefix or raw.startswith(alias_prefix + "/"): - rest = raw[len(alias_prefix):].lstrip("/") - first = None - for base in alias_bases: - cand = Path(os.path.normpath(Path(base) / rest)) - resolved = _resolve_js_import_path(cand) - if resolved.is_file(): - return resolved - if first is None: - first = cand - return first - return None - - -def _find_workspace_root(start_dir: Path) -> Path | None: - current = start_dir.resolve() - for candidate in [current, *current.parents]: - if (candidate / "pnpm-workspace.yaml").exists(): - return candidate - package_json = candidate / "package.json" - if package_json.is_file(): - try: - data = json.loads(package_json.read_text(encoding="utf-8")) - except Exception: - continue - if "workspaces" in data: - return candidate - return None - - -def _pnpm_workspace_globs(workspace_file: Path) -> list[str]: - globs: list[str] = [] - in_packages = False - for raw_line in workspace_file.read_text(encoding="utf-8", errors="replace").splitlines(): - line = raw_line.strip() - if not line or line.startswith("#"): - continue - if line.startswith("packages:"): - in_packages = True - continue - if in_packages and line.startswith("-"): - value = line[1:].strip().strip("'\"") - if value and not value.startswith("!"): - globs.append(value) - continue - if in_packages and not raw_line.startswith((" ", "\t")): - break - return globs - - -def _workspace_globs(root: Path) -> list[str]: - pnpm_workspace = root / "pnpm-workspace.yaml" - if pnpm_workspace.exists(): - return _pnpm_workspace_globs(pnpm_workspace) - - package_json = root / "package.json" - try: - data = json.loads(package_json.read_text(encoding="utf-8")) - except Exception: - return [] - - workspaces = data.get("workspaces") - if isinstance(workspaces, list): - return [item for item in workspaces if isinstance(item, str) and not item.startswith("!")] - if isinstance(workspaces, dict): - packages = workspaces.get("packages") - if isinstance(packages, list): - return [item for item in packages if isinstance(item, str) and not item.startswith("!")] - return [] - - -def _load_workspace_packages(start_dir: Path) -> dict[str, Path]: - root = _find_workspace_root(start_dir) - if root is None: - return {} - manifest_mtimes = tuple( - (name, (root / name).stat().st_mtime_ns) - for name in _WORKSPACE_MANIFEST_NAMES - if (root / name).is_file() - ) - key = str((root, manifest_mtimes)) - if key in _WORKSPACE_PACKAGE_CACHE: - return _WORKSPACE_PACKAGE_CACHE[key] - - packages: dict[str, Path] = {} - for pattern in _workspace_globs(root): - package_dirs: list[Path] = [root] if pattern in (".", "./") else list(root.glob(pattern)) - for package_dir in package_dirs: - manifest = package_dir / "package.json" - if not manifest.is_file(): - continue - try: - data = json.loads(manifest.read_text(encoding="utf-8")) - except Exception: - continue - name = data.get("name") - if isinstance(name, str) and name: - packages[name] = package_dir - _WORKSPACE_PACKAGE_CACHE[key] = packages - return packages - # Condition keys consulted when resolving an `exports` target, in priority # order. `default` is Node's catch-all and must be consulted LAST so a more # specific condition (source/import/module/etc.) wins when several match. -_EXPORT_CONDITION_PRIORITY = ( - "source", "import", "module", "svelte", "types", "require", "default", -) - - -def _resolve_export_target(value: Any) -> str | None: - """Resolve an `exports` map value (string or condition object) to a - relative target string, honouring _EXPORT_CONDITION_PRIORITY for objects - and recursing into nested condition objects.""" - if isinstance(value, str): - return value - if isinstance(value, dict): - for cond in _EXPORT_CONDITION_PRIORITY: - v = value.get(cond) - if isinstance(v, str): - return v - if isinstance(v, dict): - nested = _resolve_export_target(v) - if nested: - return nested - return None - - -def _contained_in_package(resolved: Path, package_dir: Path) -> bool: - """Guard against `exports` targets that escape the package directory - (e.g. "./evil": "../../../etc/passwd"). Only accept paths that stay - within package_dir after resolution.""" - try: - return resolved.resolve().is_relative_to(package_dir.resolve()) - except ValueError: - return False - - -def _package_entry_candidates(package_dir: Path, subpath: str) -> list[Path]: - manifest = package_dir / "package.json" - manifest_data: dict[str, Any] = {} - try: - manifest_data = json.loads(manifest.read_text(encoding="utf-8")) - except Exception: - pass - - if subpath: - # Consult the package's `exports` subpath map before the bare-path - # fallback (#1308): "./browser" -> conditions -> file, plus single - # wildcard "./*" patterns. Targets that escape the package dir are - # rejected; resolution then falls through to the bare path. - exports = manifest_data.get("exports") - if isinstance(exports, dict): - subpath_key = "./" + subpath - target = _resolve_export_target(exports.get(subpath_key)) - if target: - candidate = package_dir / target - if _contained_in_package(candidate, package_dir): - return [candidate] - else: - for pattern, pattern_value in exports.items(): - if "*" in pattern and pattern.count("*") == 1: - prefix, suffix = pattern.split("*", 1) - if (subpath_key.startswith(prefix) - and (not suffix or subpath_key.endswith(suffix))): - matched = subpath_key[len(prefix):len(subpath_key) - len(suffix) if suffix else None] - resolved = _resolve_export_target(pattern_value) - if resolved and "*" in resolved: - candidate = package_dir / resolved.replace("*", matched) - if _contained_in_package(candidate, package_dir): - return [candidate] - return [package_dir / subpath] - - exports = manifest_data.get("exports") - if isinstance(exports, str): - return [package_dir / exports] - if isinstance(exports, dict): - dot_target = _resolve_export_target(exports.get(".")) - if dot_target: - return [package_dir / dot_target] - - candidates: list[Path] = [] - for key in ("svelte", "module", "main", "types"): - value = manifest_data.get(key) - if isinstance(value, str): - candidates.append(package_dir / value) - candidates.append(package_dir / "src/index") - candidates.append(package_dir / "index") - return candidates - - -def _resolve_workspace_import(raw: str, start_dir: Path) -> Path | None: - packages = _load_workspace_packages(start_dir) - for package_name, package_dir in packages.items(): - if raw == package_name: - subpath = "" - elif raw.startswith(package_name + "/"): - subpath = raw[len(package_name) + 1:] - else: - continue - for candidate in _package_entry_candidates(package_dir, subpath): - resolved = _resolve_js_import_path(candidate) - if resolved.is_file(): - return resolved - return None - - -def _resolve_js_module_path(raw: str | Path, start_dir: Path | None = None) -> Path | None: - """Resolve a JS/TS module path or specifier to a local source file. - - With a Path argument this preserves the path-based helper API used by - import-extension tests. With a string plus start_dir it resolves JS/TS - module specifiers including relative paths, tsconfig aliases, and workspace - packages. - """ - if isinstance(raw, Path): - return _resolve_js_import_path(raw) - if start_dir is None: - return _resolve_js_import_path(Path(raw)) - if raw.startswith("."): - return _resolve_js_import_path(start_dir / raw) - - aliases = _load_tsconfig_aliases(start_dir) - hit = _resolve_tsconfig_alias(raw, aliases) - if hit is not None: - return _resolve_js_import_path(hit) - - return _resolve_workspace_import(raw, start_dir) # ── LanguageConfig dataclass ───────────────────────────────────────────────── -@dataclass -class LanguageConfig: - ts_module: str # e.g. "tree_sitter_python" - ts_language_fn: str = "language" # attr to call: e.g. tslang.language() - - class_types: frozenset = frozenset() - function_types: frozenset = frozenset() - import_types: frozenset = frozenset() - call_types: frozenset = frozenset() - static_prop_types: frozenset = frozenset() - helper_fn_names: frozenset = frozenset() - container_bind_methods: frozenset = frozenset() - event_listener_properties: frozenset = frozenset() - - # Name extraction - name_field: str = "name" - name_fallback_child_types: tuple = () - - # Body detection - body_field: str = "body" - body_fallback_child_types: tuple = () # e.g. ("declaration_list", "compound_statement") - - # Call name extraction - call_function_field: str = "function" # field on call node for callee - call_accessor_node_types: frozenset = frozenset() # member/attribute nodes - call_accessor_field: str = "attribute" # field on accessor for method name - call_accessor_object_field: str = "" # field on accessor for the receiver/object - - # Stop recursion at these types in walk_calls - function_boundary_types: frozenset = frozenset() - - # Import handler: called for import nodes instead of generic handling - import_handler: Callable | None = None - - # Optional custom name resolver for functions (C, C++ declarator unwrapping) - resolve_function_name_fn: Callable | None = None - - # Extra label formatting for functions: if True, functions get "name()" label - function_label_parens: bool = True - - # Extra walk hook called after generic dispatch (for JS arrow functions, C# namespaces, etc.) - extra_walk_fn: Callable | None = None - # ── Generic helpers ─────────────────────────────────────────────────────────── - -_PYTHON_TYPE_CONTAINERS = frozenset({ - "list", "dict", "set", "tuple", "frozenset", "type", - "List", "Dict", "Set", "Tuple", "FrozenSet", "Type", - "Optional", "Union", "Sequence", "Iterable", "Mapping", "MutableMapping", - "Iterator", "Callable", "Awaitable", "AsyncIterable", "AsyncIterator", "Coroutine", - "Generator", "AsyncGenerator", "ContextManager", "AsyncContextManager", - "Annotated", "ClassVar", "Final", "Literal", "Concatenate", "ParamSpec", "TypeVar", - "None", "Ellipsis", -}) - # Scalar builtins and test-mock names that appear as type annotations but carry # no useful semantic meaning as graph nodes (#1147). Suppressed at the annotation # walker level so they are never created as nodes or emitted as edges. -_PYTHON_ANNOTATION_NOISE = frozenset({ - # scalar builtins - "str", "int", "float", "bool", "bytes", "bytearray", "complex", "object", - "True", "False", - # unittest.mock - "MagicMock", "Mock", "AsyncMock", "NonCallableMock", - "NonCallableMagicMock", "PropertyMock", "patch", "sentinel", -}) - - -def _python_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: - """Walk a Python type annotation; append (name, role) where role is 'type' or 'generic_arg'. - - Builtin/typing containers (list, dict, Optional, Union, …) are not emitted as refs themselves, - but their nested type arguments still count as generic_arg. - """ - if node is None: - return - t = node.type - if t == "type": - for c in node.children: - if c.is_named: - _python_collect_type_refs(c, source, generic, out) - return - if t == "identifier": - name = _read_text(node, source) - if name and name not in _PYTHON_TYPE_CONTAINERS and name not in _PYTHON_ANNOTATION_NOISE: - out.append((name, "generic_arg" if generic else "type")) - return - if t == "attribute": - tail = _read_text(node, source).rsplit(".", 1)[-1] - if tail and tail not in _PYTHON_TYPE_CONTAINERS and tail not in _PYTHON_ANNOTATION_NOISE: - out.append((tail, "generic_arg" if generic else "type")) - return - if t == "generic_type": - for c in node.children: - if c.type == "identifier": - container = _read_text(c, source) - if container and container not in _PYTHON_TYPE_CONTAINERS and container not in _PYTHON_ANNOTATION_NOISE: - out.append((container, "generic_arg" if generic else "type")) - elif c.type == "type_parameter": - for sub in c.children: - if sub.is_named: - _python_collect_type_refs(sub, source, True, out) - return - if t == "subscript": - value = node.child_by_field_name("value") - if value is not None: - _python_collect_type_refs(value, source, generic, out) - for c in node.children: - if c is value or not c.is_named: - continue - _python_collect_type_refs(c, source, True, out) - return - if node.is_named: - for c in node.children: - if c.is_named: - _python_collect_type_refs(c, source, generic, out) - - -def _csharp_pre_scan_interfaces(root_node, source: bytes) -> set[str]: - """Return names declared as `interface` in this C# compilation unit.""" - out: set[str] = set() - stack = [root_node] - while stack: - n = stack.pop() - if n.type == "interface_declaration": - name_node = n.child_by_field_name("name") - if name_node is not None: - text = _read_text(name_node, source) - if text: - out.add(text) - stack.extend(n.children) - return out - - -def _csharp_classify_base(name: str, interface_names: set[str]) -> str: - """`implements` if the base name is an interface (declared or by I-prefix convention), else `inherits`.""" - if name in interface_names: - return "implements" - if len(name) >= 2 and name[0] == "I" and name[1].isupper(): - return "implements" - return "inherits" - - -def _csharp_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: - """Walk a C# type expression; append (name, role) tuples (role is 'type' or 'generic_arg').""" - if node is None: - return - t = node.type - if t == "predefined_type": - return - if t == "identifier": - name = _read_text(node, source) - if name: - out.append((name, "generic_arg" if generic else "type")) - return - if t == "qualified_name": - text = _read_text(node, source).rsplit(".", 1)[-1] - if text: - out.append((text, "generic_arg" if generic else "type")) - return - if t == "generic_name": - name_child = node.child_by_field_name("name") - if name_child is None: - for sub in node.children: - if sub.type == "identifier": - name_child = sub - break - if name_child is not None: - name = _read_text(name_child, source) - if name: - out.append((name, "generic_arg" if generic else "type")) - for sub in node.children: - if sub.type == "type_argument_list": - for arg in sub.children: - if arg.is_named: - _csharp_collect_type_refs(arg, source, True, out) - return - if t in ("nullable_type", "array_type", "pointer_type", "ref_type"): - for c in node.children: - if c.is_named: - _csharp_collect_type_refs(c, source, generic, out) - return - if node.is_named: - for c in node.children: - if c.is_named: - _csharp_collect_type_refs(c, source, generic, out) - - -def _csharp_attribute_names(method_node, source: bytes) -> list[str]: - """Collect attribute names from a C# method/declaration's attribute_list children.""" - names: list[str] = [] - for child in method_node.children: - if child.type != "attribute_list": - continue - for attr in child.children: - if attr.type != "attribute": - continue - name_node = attr.child_by_field_name("name") - if name_node is None: - for sub in attr.children: - if sub.type in ("identifier", "qualified_name"): - name_node = sub - break - if name_node is not None: - text = _read_text(name_node, source).rsplit(".", 1)[-1] - if text: - names.append(text) - return names -_JAVA_TYPE_PARAMETER_SCOPE_DECLARATIONS = frozenset({ - "class_declaration", - "interface_declaration", - "record_declaration", - "method_declaration", - "constructor_declaration", -}) - +# java.lang (auto-imported) plus the ubiquitous java.util / java.io / java.time / +# java.util.{stream,function,concurrent} / java.math / java.nio.file types that +# appear as field, parameter, return, and generic-argument annotations. They never +# resolve to a project node, so emitting `references` edges to them is pure noise +# (mirrors _GO_PREDECLARED_TYPES / _PYTHON_ANNOTATION_NOISE). Suppressed at the +# type-ref walker so they are never created as nodes or emitted as edges. The +# boxed-scalar/`void` primitives are already dropped by grammar node type above; +# these are the class/interface names the grammar reports as identifiers. -def _java_type_parameters_in_scope(node, source: bytes) -> frozenset[str]: - """Return Java type-parameter names visible from ``node``.""" - names: set[str] = set() - scope = node - while scope is not None: - if scope.type in _JAVA_TYPE_PARAMETER_SCOPE_DECLARATIONS: - params = scope.child_by_field_name("type_parameters") - if params is not None: - for param in params.children: - if param.type != "type_parameter": - continue - name_node = next( - (child for child in param.children if child.type == "type_identifier"), - None, - ) - if name_node is not None: - names.add(_read_text(name_node, source)) - scope = scope.parent - return frozenset(names) - - -def _java_collect_type_refs( - node, - source: bytes, - generic: bool, - out: list[tuple[str, str]], - skip: frozenset[str] | None = None, -) -> None: - """Walk a Java type expression; append (name, role) tuples.""" - if node is None: - return - if skip is None: - skip = _java_type_parameters_in_scope(node, source) - t = node.type - if t in ("integral_type", "floating_point_type", "boolean_type", "void_type"): - return - if t == "type_identifier": - name = _read_text(node, source) - if name and name not in skip: - out.append((name, "generic_arg" if generic else "type")) - return - if t == "scoped_type_identifier": - text = _read_text(node, source).rsplit(".", 1)[-1] - if text: - out.append((text, "generic_arg" if generic else "type")) - return - if t == "generic_type": - for c in node.children: - if c.type in ("type_identifier", "scoped_type_identifier"): - text = _read_text(c, source).rsplit(".", 1)[-1] - if text and (c.type == "scoped_type_identifier" or text not in skip): - out.append((text, "generic_arg" if generic else "type")) - break - for c in node.children: - if c.type == "type_arguments": - for arg in c.children: - if arg.is_named: - _java_collect_type_refs(arg, source, True, out, skip) - return - if t == "array_type": - for c in node.children: - if c.is_named: - _java_collect_type_refs(c, source, generic, out, skip) - return - if node.is_named: - for c in node.children: - if c.is_named: - _java_collect_type_refs(c, source, generic, out, skip) +# ── C / C++ type-ref helpers ───────────────────────────────────────────────── -def _java_annotation_names(declaration_node, source: bytes) -> list[str]: - """Collect annotation names from a Java declaration's `modifiers` child.""" - names: list[str] = [] - modifiers = None - for child in declaration_node.children: - if child.type == "modifiers": - modifiers = child - break - if modifiers is None: - return names - for anno in modifiers.children: - if anno.type not in ("marker_annotation", "annotation"): - continue - name_node = anno.child_by_field_name("name") - if name_node is None: - for sub in anno.children: - if sub.type in ("identifier", "scoped_identifier", "type_identifier"): - name_node = sub - break - if name_node is not None: - text = _read_text(name_node, source).rsplit(".", 1)[-1] - if text: - names.append(text) - return names +# ── Scala type-ref helpers ─────────────────────────────────────────────────── -_GO_PREDECLARED_TYPES = frozenset({ - "bool", "byte", "complex64", "complex128", "error", "float32", "float64", - "int", "int8", "int16", "int32", "int64", "rune", "string", - "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "any", "comparable", -}) +def _resolve_name(node, source: bytes, config: LanguageConfig) -> str | None: + """Get the name from a node using config.name_field, falling back to child types.""" + if config.resolve_function_name_fn is not None: + # For C/C++ where the name is inside a declarator + return None # caller handles this separately + n = node.child_by_field_name(config.name_field) + if n: + return _read_text(n, source) + for child in node.children: + if child.type in config.name_fallback_child_types: + return _read_text(child, source) + return None -def _go_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: - """Walk a Go type expression; append (name, role) tuples.""" - if node is None: - return - t = node.type - if t == "type_identifier": - text = _read_text(node, source) - if text and text not in _GO_PREDECLARED_TYPES: - out.append((text, "generic_arg" if generic else "type")) - return - if t == "qualified_type": - text = _read_text(node, source).rsplit(".", 1)[-1] - if text and text not in _GO_PREDECLARED_TYPES: - out.append((text, "generic_arg" if generic else "type")) - return - if t == "generic_type": - type_field = node.child_by_field_name("type") - if type_field is not None: - sub: list[tuple[str, str]] = [] - _go_collect_type_refs(type_field, source, generic, sub) - out.extend(sub) - for c in node.children: - if c.type == "type_arguments": - for arg in c.children: - if arg.is_named: - _go_collect_type_refs(arg, source, True, out) - return - if t in ("pointer_type", "slice_type", "array_type", "map_type", - "channel_type", "parenthesized_type"): - for c in node.children: - if c.is_named: - _go_collect_type_refs(c, source, generic, out) - return - if node.is_named: - for c in node.children: - if c.is_named: - _go_collect_type_refs(c, source, generic, out) +# ── Import handlers ─────────────────────────────────────────────────────────── -def _rust_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: - """Walk a Rust type expression; append (name, role) tuples.""" - if node is None: - return +def _import_python(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str, scope_stack: list[str] | None = None) -> None: t = node.type - if t == "primitive_type": - return - if t == "type_identifier": - text = _read_text(node, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - return - if t == "scoped_type_identifier": - text = _read_text(node, source).rsplit("::", 1)[-1] - if text: - out.append((text, "generic_arg" if generic else "type")) - return - if t == "generic_type": - name_node = node.child_by_field_name("type") - if name_node is None: - for c in node.children: - if c.type in ("type_identifier", "scoped_type_identifier"): - name_node = c - break - if name_node is not None: - text = _read_text(name_node, source).rsplit("::", 1)[-1] - if text: - out.append((text, "generic_arg" if generic else "type")) - for c in node.children: - if c.type == "type_arguments": - for arg in c.children: - if arg.is_named: - _rust_collect_type_refs(arg, source, True, out) - return - if t in ("reference_type", "pointer_type", "array_type", "tuple_type", "slice_type"): - for c in node.children: - if c.is_named: - _rust_collect_type_refs(c, source, generic, out) - return - if node.is_named: - for c in node.children: - if c.is_named: - _rust_collect_type_refs(c, source, generic, out) - - -def _php_name_text(node, source: bytes) -> str | None: - """Return the unqualified name text from a PHP `name`/`qualified_name` node.""" - if node is None: - return None - return _read_text(node, source).rsplit("\\", 1)[-1] or None + if t == "import_statement": + for child in node.children: + if child.type in ("dotted_name", "aliased_import"): + raw = _read_text(child, source) + module_name = raw.split(" as ")[0].strip().lstrip(".") + tgt_nid = _make_id(module_name) + edges.append({ + "source": file_nid, + "target": tgt_nid, + "relation": "imports", + "context": "import", + "confidence": "EXTRACTED", + "source_file": str_path, + "source_location": f"L{node.start_point[0] + 1}", + "weight": 1.0, + }) + elif t == "import_from_statement": + module_node = node.child_by_field_name("module_name") + if module_node: + raw = _read_text(module_node, source) + if raw.startswith("."): + # Relative import - resolve to full path so IDs match file node IDs + dots = len(raw) - len(raw.lstrip(".")) + module_name = raw.lstrip(".") + base = Path(str_path).parent + for _ in range(dots - 1): + base = base.parent + rel = (module_name.replace(".", "/") + ".py") if module_name else "__init__.py" + tgt_nid = _make_id(str(base / rel)) + else: + tgt_nid = _make_id(raw) + edges.append({ + "source": file_nid, + "target": tgt_nid, + "relation": "imports_from", + "context": "import", + "confidence": "EXTRACTED", + "source_file": str_path, + "source_location": f"L{node.start_point[0] + 1}", + "weight": 1.0, + }) -def _php_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: - """Walk a PHP type expression; append (name, role) tuples.""" - if node is None: - return - t = node.type - if t == "primitive_type": - return - if t == "named_type": - for c in node.children: - if c.type in ("name", "qualified_name"): - text = _php_name_text(c, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - return - return - if t in ("name", "qualified_name"): - text = _php_name_text(node, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - return - if t in ("nullable_type", "union_type", "intersection_type", "optional_type"): - for c in node.children: - if c.is_named: - _php_collect_type_refs(c, source, generic, out) - return - if node.is_named: - for c in node.children: - if c.is_named: - _php_collect_type_refs(c, source, generic, out) - - -def _php_method_return_type_node(method_node): - """Return the named_type/primitive_type node sitting after formal_parameters.""" - saw_params = False - for c in method_node.children: - if c.type == "formal_parameters": - saw_params = True - continue - if saw_params and c.is_named and c.type not in ("compound_statement",): - if c.type in ("named_type", "primitive_type", "nullable_type", - "union_type", "intersection_type", "optional_type"): - return c - return None - - -def _kotlin_user_type_name(user_type_node, source: bytes) -> str | None: - """Return the head identifier text from a Kotlin user_type node (without generics).""" - if user_type_node is None: - return None - for c in user_type_node.children: - if c.type == "type_identifier": - text = _read_text(c, source) - return text or None - if c.type == "identifier": - text = _read_text(c, source) - return text or None - if c.type == "simple_user_type": - for sub in c.children: - if sub.type in ("identifier", "type_identifier"): - text = _read_text(sub, source) - return text or None - return None - - -def _kotlin_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: - """Walk a Kotlin type expression; append (name, role) tuples.""" - if node is None: - return - t = node.type - if t in ("integral_literal", "boolean_literal"): - return - if t == "user_type": - for c in node.children: - if c.type in ("identifier", "type_identifier"): - text = _read_text(c, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - break - if c.type == "simple_user_type": - for sub in c.children: - if sub.type in ("identifier", "type_identifier"): - text = _read_text(sub, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - break - break - for c in node.children: - if c.type == "type_arguments": - for arg in c.children: - if arg.type == "type_projection": - for sub in arg.children: - if sub.is_named: - _kotlin_collect_type_refs(sub, source, True, out) - elif arg.is_named: - _kotlin_collect_type_refs(arg, source, True, out) - return - if t in ("identifier", "type_identifier"): - text = _read_text(node, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - return - if t in ("nullable_type", "parenthesized_type", "type_reference"): - for c in node.children: - if c.is_named: - _kotlin_collect_type_refs(c, source, generic, out) - return - if node.is_named: - for c in node.children: - if c.is_named: - _kotlin_collect_type_refs(c, source, generic, out) - - -def _kotlin_property_type_node(property_node): - """Find the user_type node within a Kotlin property_declaration.""" - for c in property_node.children: - if c.type == "variable_declaration": - for sub in c.children: - if sub.type in ("user_type", "nullable_type", "type_reference"): - return sub - if c.type in ("user_type", "nullable_type", "type_reference"): - return c - return None - - -def _kotlin_function_return_type_node(func_node): - """Find the return-type node of a Kotlin function_declaration (the type after `: ` post-params).""" - saw_params = False - saw_colon = False - for c in func_node.children: - if c.type == "function_value_parameters": - saw_params = True - continue - if saw_params and c.type == ":": - saw_colon = True - continue - if saw_colon: - if c.is_named: - return c - return None - - -def _swift_declaration_keyword(node) -> str | None: - """Return the leading kind token for a Swift class_declaration: class/struct/enum/extension/actor.""" - for c in node.children: - if not c.is_named and c.type in ("class", "struct", "enum", "extension", "actor"): - return c.type - return None - - -def _swift_pre_scan(root_node, source: bytes) -> tuple[set[str], set[str]]: - """Pre-scan a Swift compilation unit and return (protocol_names, class_like_names).""" - protocols: set[str] = set() - classes: set[str] = set() - stack = [root_node] - while stack: - n = stack.pop() - if n.type == "protocol_declaration": - name_node = n.child_by_field_name("name") - if name_node is None: - for c in n.children: - if c.type == "type_identifier": - name_node = c - break - if name_node is not None: - text = _read_text(name_node, source) - if text: - protocols.add(text) - elif n.type == "class_declaration": - kw = _swift_declaration_keyword(n) - if kw in ("class", "struct", "enum", "actor"): - name_node = n.child_by_field_name("name") - if name_node is not None: - text = _read_text(name_node, source) - if text: - classes.add(text) - stack.extend(n.children) - return protocols, classes - - -def _swift_classify_base(name: str, kind: str | None, is_first: bool, - protocols: set[str], classes: set[str]) -> str: - """Classify a Swift inheritance_specifier entry as `inherits` or `implements`.""" - if name in protocols: - return "implements" - if name in classes: - return "inherits" - # struct/enum/extension/actor cannot inherit a class — all conformances are protocols. - if kind in ("struct", "enum", "extension", "actor"): - return "implements" - # `class`: first entry is conventionally the base class; subsequent are protocols. - return "inherits" if is_first else "implements" - - -def _swift_user_type_name(user_type_node, source: bytes) -> str | None: - """Return the head type_identifier text from a Swift user_type node (without generics).""" - if user_type_node is None: - return None - for c in user_type_node.children: - if c.type == "type_identifier": - text = _read_text(c, source) - return text or None - return None - - -def _swift_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: - """Walk a Swift type expression; append (name, role) tuples (role 'type' or 'generic_arg').""" - if node is None: - return - t = node.type - if t == "type_annotation": - for c in node.children: - if c.is_named: - _swift_collect_type_refs(c, source, generic, out) - return - if t == "user_type": - for c in node.children: - if c.type == "type_identifier": - text = _read_text(c, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - break - for c in node.children: - if c.type == "type_arguments": - for arg in c.children: - if arg.is_named: - _swift_collect_type_refs(arg, source, True, out) - return - if t == "type_identifier": - text = _read_text(node, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - return - if t in ("optional_type", "implicitly_unwrapped_optional_type", "array_type", - "dictionary_type", "tuple_type"): - for c in node.children: - if c.is_named: - _swift_collect_type_refs(c, source, generic, out) - return - if node.is_named: - for c in node.children: - if c.is_named: - _swift_collect_type_refs(c, source, generic, out) - - -def _swift_property_type_node(property_node): - """Return the type_annotation child of a Swift property_declaration, if any.""" - for c in property_node.children: - if c.type == "type_annotation": - return c - return None - - -def _swift_property_name(property_node, source: bytes) -> str | None: - """Return the bound name of a Swift property (``let x``/``var x = ...``).""" - for c in property_node.children: - if c.type == "pattern": - for sc in c.children: - if sc.type == "simple_identifier": - return _read_text(sc, source) - if c.type == "simple_identifier": - return _read_text(c, source) - return None - - -def _swift_constructor_type(call_node, source: bytes) -> str | None: - """If a Swift call expression is a constructor (``Foo()``), return the type name. - - Only upper-cased callees are treated as types so a free-function call like - ``configure()`` in an initializer is not mistaken for a constructor. - """ - first = call_node.children[0] if call_node.children else None - if first is not None and first.type == "simple_identifier": - text = _read_text(first, source) - if text and text[:1].isupper(): - return text - return None - - -def _swift_receiver_name(recv_node, source: bytes) -> str | None: - """Return the depth-1 receiver name of a Swift member call (``recv.method()``). - - ``vm.update()`` -> ``vm``; ``Type.staticMethod()`` -> ``Type``; - ``Singleton.shared.method()`` -> ``Singleton`` (head of the chain); - ``self.svc.fetch()`` -> ``svc`` (the property the call is reached through). - Returns None for anything deeper, so resolution stays depth-1. - """ - if recv_node is None: - return None - if recv_node.type == "simple_identifier": - return _read_text(recv_node, source) - if recv_node.type == "navigation_expression": - head = recv_node.children[0] if recv_node.children else None - if head is not None and head.type == "simple_identifier": - return _read_text(head, source) - if head is not None and head.type == "self_expression": - for child in recv_node.children: - if child.type == "navigation_suffix": - for sc in child.children: - if sc.type == "simple_identifier": - return _read_text(sc, source) - return None - - -# ── C / C++ type-ref helpers ───────────────────────────────────────────────── - -_C_PRIMITIVE_TYPE_NODES = frozenset({ - "primitive_type", "sized_type_specifier", "auto", "placeholder_type_specifier", -}) - - -def _c_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: - """Walk a C type expression; append (name, role) tuples for user-defined types. - Skips primitive types and qualifiers; recognises type_identifier.""" - if node is None or node.type in _C_PRIMITIVE_TYPE_NODES: - return - t = node.type - if t == "type_identifier": - text = _read_text(node, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - return - if t in ("pointer_declarator", "reference_declarator", "array_declarator", - "type_qualifier", "type_descriptor", "abstract_pointer_declarator", - "abstract_reference_declarator", "abstract_array_declarator"): - for c in node.children: - if c.is_named: - _c_collect_type_refs(c, source, generic, out) - - -def _cpp_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: - """Walk a C++ type expression; append (name, role) tuples. - Resolves qualified_identifier tails (std::string → string) and template_type - base + arguments (std::vector → vector + HttpClient as generic_arg).""" - if node is None or node.type in _C_PRIMITIVE_TYPE_NODES: - return - t = node.type - if t == "type_identifier": - text = _read_text(node, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - return - if t == "qualified_identifier": - name_node = node.child_by_field_name("name") - if name_node is not None: - _cpp_collect_type_refs(name_node, source, generic, out) - return - if t == "template_type": - name_node = node.child_by_field_name("name") - if name_node is not None: - text = _read_text(name_node, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - args_node = node.child_by_field_name("arguments") - if args_node is not None: - for c in args_node.children: - if c.is_named: - _cpp_collect_type_refs(c, source, True, out) - return - if t in ("type_descriptor", "pointer_declarator", "reference_declarator", - "array_declarator", "type_qualifier", "abstract_pointer_declarator", - "abstract_reference_declarator", "abstract_array_declarator"): - for c in node.children: - if c.is_named: - _cpp_collect_type_refs(c, source, generic, out) - - -# ── Scala type-ref helpers ─────────────────────────────────────────────────── - -def _scala_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: - """Walk a Scala type expression; append (name, role) tuples. - Handles type_identifier, generic_type (List[T]), and common type wrappers.""" - if node is None: - return - t = node.type - if t == "type_identifier": - text = _read_text(node, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - return - if t == "generic_type": - base = node.child_by_field_name("type") - if base is None: - for c in node.children: - if c.type == "type_identifier": - base = c - break - if base is not None and base.type == "type_identifier": - text = _read_text(base, source) - if text: - out.append((text, "generic_arg" if generic else "type")) - for c in node.children: - if c.type == "type_arguments": - for arg in c.children: - if arg.is_named: - _scala_collect_type_refs(arg, source, True, out) - return - if t in ("compound_type", "infix_type", "function_type", "tuple_type", - "annotated_type", "projected_type"): - for c in node.children: - if c.is_named: - _scala_collect_type_refs(c, source, generic, out) - - -def _python_collect_param_refs(params_node, source: bytes) -> list[tuple[str, str]]: - """Collect type refs from each typed parameter under a `parameters` node.""" - out: list[tuple[str, str]] = [] - if params_node is None: - return out - for child in params_node.children: - if child.type in ("typed_parameter", "typed_default_parameter"): - type_node = child.child_by_field_name("type") - _python_collect_type_refs(type_node, source, False, out) - return out - - -def _resolve_name(node, source: bytes, config: LanguageConfig) -> str | None: - """Get the name from a node using config.name_field, falling back to child types.""" - if config.resolve_function_name_fn is not None: - # For C/C++ where the name is inside a declarator - return None # caller handles this separately - n = node.child_by_field_name(config.name_field) - if n: - return _read_text(n, source) - for child in node.children: - if child.type in config.name_fallback_child_types: - return _read_text(child, source) - return None - - -def _find_body(node, config: LanguageConfig): - """Find the body node using config.body_field, falling back to child types.""" - b = node.child_by_field_name(config.body_field) - if b: - return b - for child in node.children: - if child.type in config.body_fallback_child_types: - return child - return None - - -# ── Import handlers ─────────────────────────────────────────────────────────── - -def _import_python(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None: - t = node.type - if t == "import_statement": - for child in node.children: - if child.type in ("dotted_name", "aliased_import"): - raw = _read_text(child, source) - module_name = raw.split(" as ")[0].strip().lstrip(".") - tgt_nid = _make_id(module_name) - edges.append({ - "source": file_nid, - "target": tgt_nid, - "relation": "imports", - "context": "import", - "confidence": "EXTRACTED", - "source_file": str_path, - "source_location": f"L{node.start_point[0] + 1}", - "weight": 1.0, - }) - elif t == "import_from_statement": - module_node = node.child_by_field_name("module_name") - if module_node: - raw = _read_text(module_node, source) - if raw.startswith("."): - # Relative import - resolve to full path so IDs match file node IDs - dots = len(raw) - len(raw.lstrip(".")) - module_name = raw.lstrip(".") - base = Path(str_path).parent - for _ in range(dots - 1): - base = base.parent - rel = (module_name.replace(".", "/") + ".py") if module_name else "__init__.py" - tgt_nid = _make_id(str(base / rel)) - else: - tgt_nid = _make_id(raw) - edges.append({ - "source": file_nid, - "target": tgt_nid, - "relation": "imports_from", - "context": "import", - "confidence": "EXTRACTED", - "source_file": str_path, - "source_location": f"L{node.start_point[0] + 1}", - "weight": 1.0, - }) - - -def _resolve_js_import_target(raw: str, str_path: str) -> "tuple[str, Path | None] | None": - """Resolve a JS/TS import path string to (target_nid, resolved_path). - - Handles relative paths, tsconfig path aliases, workspace packages, and - bare/scoped imports. - Returns None if `raw` is empty. - """ - if not raw: - return None - resolved_path = _resolve_js_module_path(raw, Path(str_path).parent) - if resolved_path is not None: - return _make_id(str(resolved_path)), resolved_path - module_name = raw.split("/")[-1] - if not module_name: - return None - return _make_id(module_name), None - - -def _import_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None: - is_reexport = node.type == "export_statement" - # Only handle export_statement if it has a `from` clause (re-export). - # Pure exports like `export const x = 1` or `export { localVar }` have no source module. - if is_reexport: - has_from = any(child.type == "from" or (_read_text(child, source) == "from") for child in node.children if child.type in ("from", "identifier")) - if not has_from: - # Check for string child (source path) as a more reliable indicator - has_from = any(child.type == "string" for child in node.children) - if not has_from: +def _import_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str, scope_stack: list[str] | None = None) -> None: + is_reexport = node.type == "export_statement" + # Only handle export_statement if it has a `from` clause (re-export). + # Pure exports like `export const x = 1` or `export { localVar }` have no source module. + if is_reexport: + has_from = any(child.type == "from" or (_read_text(child, source) == "from") for child in node.children if child.type in ("from", "identifier")) + if not has_from: + # Check for string child (source path) as a more reliable indicator + has_from = any(child.type == "string" for child in node.children) + if not has_from: return resolved_path: "Path | None" = None + module_string = None for child in node.children: if child.type == "string": - raw = _read_text(child, source).strip("'\"` ") - resolved = _resolve_js_import_target(raw, str_path) - if resolved is None: - break + module_string = child + break + if child.type == "import_require_clause": + # TS import-equals form: `import x = require("./m")`. The module + # string sits inside the clause, not on the import_statement + # itself, so the direct-child scan above never sees it. + module_string = next( + (sub for sub in child.children if sub.type == "string"), None + ) + break + if module_string is not None: + raw = _read_text(module_string, source).strip("'\"` ") + resolved = _resolve_js_import_target(raw, str_path) + if resolved is not None: tgt_nid, resolved_path = resolved edges.append({ "source": file_nid, @@ -1472,7 +317,6 @@ def _import_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_p "source_location": f"L{node.start_point[0] + 1}", "weight": 1.0, }) - break # Emit symbol-level edges for named imports/re-exports from local/aliased files. # e.g. `import { Foo, type Bar } from './bar'` → file → Foo, file → Bar (EXTRACTED) @@ -1529,69 +373,7 @@ def _import_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_p }) -def _dynamic_import_js(node, source: bytes, caller_nid: str, str_path: str, edges: list, - seen_dyn_pairs: set) -> bool: - """Detect dynamic import() calls in JS/TS and emit imports_from edges. - - Handles patterns like: - await import('./foo.js') - import('./foo.js').then(...) - const m = await import(`./foo`) - - Returns True if the node was a dynamic import (caller should skip normal call handling). - """ - # Dynamic import is a call_expression whose function child is the keyword "import". - # tree-sitter-typescript parses `import('...')` as call_expression with first child - # being an "import" token (type="import"). - func_node = node.child_by_field_name("function") - if func_node is None: - # Fallback: check first child directly (some TS versions) - if node.children and _read_text(node.children[0], source) == "import": - func_node = node.children[0] - else: - return False - if _read_text(func_node, source) != "import": - return False - - # Extract the module path from the arguments - args = node.child_by_field_name("arguments") - if args is None: - return True # It's an import() but no args — skip - for arg in args.children: - if arg.type == "template_string": - # Skip dynamic template literals — path can't be statically resolved - if any(c.type == "template_substitution" for c in arg.children): - break - raw = _read_text(arg, source).strip("`") - elif arg.type == "string": - raw = _read_text(arg, source).strip("'\" ") - else: - continue - if not raw: - break - # Resolve path using the same logic as static imports. - resolved = _resolve_js_import_target(raw, str_path) - if resolved is None: - break - tgt_nid, _ = resolved - pair = (caller_nid, tgt_nid) - if pair not in seen_dyn_pairs: - seen_dyn_pairs.add(pair) - edges.append({ - "source": caller_nid, - "target": tgt_nid, - "relation": "imports_from", - "context": "import", - "confidence": "EXTRACTED", - "source_file": str_path, - "source_location": f"L{node.start_point[0] + 1}", - "weight": 1.0, - }) - break - return True - - -def _import_java(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None: +def _import_java(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str, scope_stack: list[str] | None = None) -> None: def _walk_scoped(n) -> str: parts: list[str] = [] cur = n @@ -1630,21 +412,7 @@ def _walk_scoped(n) -> str: break -def _resolve_c_include_path(raw: str, str_path: str) -> "Path | None": - """Resolve a quoted #include path to a real file on disk. - - Searches relative to the including file's directory. Returns None for - system headers (<...>) or paths that don't exist on disk. - """ - if not raw: - return None - candidate = (Path(str_path).parent / raw).resolve() - if candidate.is_file(): - return candidate - return None - - -def _import_c(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None: +def _import_c(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str, scope_stack: list[str] | None = None) -> None: for child in node.children: if child.type in ("string_literal", "system_lib_string", "string"): raw = _read_text(child, source).strip('"<> ') @@ -1681,27 +449,38 @@ def _import_c(node, source: bytes, file_nid: str, stem: str, edges: list, str_pa break -def _import_csharp(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None: - for child in node.children: - if child.type in ("qualified_name", "identifier", "name_equals"): - raw = _read_text(child, source) - module_name = raw.split(".")[-1].strip() - if module_name: - tgt_nid = _make_id(module_name) - edges.append({ - "source": file_nid, - "target": tgt_nid, - "relation": "imports", - "context": "import", - "confidence": "EXTRACTED", - "source_file": str_path, - "source_location": f"L{node.start_point[0] + 1}", - "weight": 1.0, - }) - break +def _import_csharp(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str, scope_stack: list[str] | None = None) -> None: + text = _read_text(node, source).strip().rstrip(";") + if text.startswith("global "): + text = text[len("global "):].strip() + if not text.startswith("using"): + return + body = text[len("using"):].strip() + using_kind, alias, target_fqn = "namespace", None, body + if body.startswith("static "): + using_kind, target_fqn = "static", body[len("static "):].strip() + elif "=" in body: + lhs, rhs = body.split("=", 1) + using_kind, alias, target_fqn = "alias", lhs.strip(), rhs.strip() + if not target_fqn: + return + edges.append({ + "source": file_nid, + "target": _make_id(target_fqn), + "relation": "imports", + "context": "import", + "confidence": "EXTRACTED", + "source_file": str_path, + "source_location": f"L{node.start_point[0] + 1}", + "weight": 1.0, + "metadata": sanitize_metadata({k: v for k, v in + {"using_kind": using_kind, "alias": alias, "target_fqn": target_fqn, + "scope_kind": "namespace" if scope_stack else "file", + "scope_id": scope_stack[-1] if scope_stack else None}.items() if v is not None}), + }) -def _import_kotlin(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None: +def _import_kotlin(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str, scope_stack: list[str] | None = None) -> None: path_node = node.child_by_field_name("path") if path_node: raw = _read_text(path_node, source) @@ -1737,7 +516,7 @@ def _import_kotlin(node, source: bytes, file_nid: str, stem: str, edges: list, s break -def _import_scala(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None: +def _import_scala(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str, scope_stack: list[str] | None = None) -> None: for child in node.children: if child.type in ("stable_id", "identifier"): raw = _read_text(child, source) @@ -1757,7 +536,7 @@ def _import_scala(node, source: bytes, file_nid: str, stem: str, edges: list, st break -def _import_php(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None: +def _import_php(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str, scope_stack: list[str] | None = None) -> None: for child in node.children: if child.type in ("qualified_name", "name", "identifier"): raw = _read_text(child, source) @@ -1792,334 +571,24 @@ def _get_c_func_name(node, source: bytes) -> str | None: return None -def _get_cpp_func_name(node, source: bytes) -> str | None: - """Recursively unwrap declarator to find the innermost identifier (C++).""" - if node.type == "identifier": - return _read_text(node, source) - if node.type in ("field_identifier", "destructor_name", "operator_name"): - return _read_text(node, source) - if node.type == "qualified_identifier": - name_node = node.child_by_field_name("name") - if name_node: - return _read_text(name_node, source) - decl = node.child_by_field_name("declarator") - if decl: - return _get_cpp_func_name(decl, source) - for child in node.children: - if child.type == "identifier": - return _read_text(child, source) - return None - - # ── JS/TS extra walk for arrow functions ────────────────────────────────────── -def _find_require_call(value_node): - """Return the call_expression node if `value_node` is a `require(...)` call - or `require(...).x` member access. Otherwise None.""" - if value_node is None: - return None - if value_node.type == "call_expression": - fn = value_node.child_by_field_name("function") - if fn is not None and fn.type == "identifier": - return value_node - if value_node.type == "member_expression": - obj = value_node.child_by_field_name("object") - return _find_require_call(obj) - return None - - -def _require_imports_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> bool: - """Detect CommonJS require imports inside lexical_declaration / variable_declaration. - - Handles three patterns: - const { foo, bar } = require('./mod') → file → mod (imports_from), file → foo, file → bar - const mod = require('./mod') → file → mod (imports_from) - const x = require('./mod').y → file → mod (imports_from), file → y - - Returns True if any require import was found. - """ - if node.type not in ("lexical_declaration", "variable_declaration"): - return False - found = False - for child in node.children: - if child.type != "variable_declarator": - continue - value = child.child_by_field_name("value") - call = _find_require_call(value) - if call is None: - continue - fn = call.child_by_field_name("function") - if fn is None or _read_text(fn, source) != "require": - continue - args = call.child_by_field_name("arguments") - if args is None: - continue - raw = None - for arg in args.children: - if arg.type == "string": - raw = _read_text(arg, source).strip("'\"` ") - break - if not raw: - continue - resolved = _resolve_js_import_target(raw, str_path) - if resolved is None: - continue - tgt_nid, resolved_path = resolved - line = node.start_point[0] + 1 - edges.append({ - "source": file_nid, - "target": tgt_nid, - "relation": "imports_from", - "context": "import", - "confidence": "EXTRACTED", - "source_file": str_path, - "source_location": f"L{line}", - "weight": 1.0, - }) - found = True - - # Symbol-level edges for destructured / accessor binders. - target_stem = _file_stem(resolved_path) if resolved_path is not None else None - name_node = child.child_by_field_name("name") - sym_names: list[str] = [] - if name_node is not None and name_node.type == "object_pattern": - # `const { a, b: alias } = require('./m')` — emit edges for each property key - for prop in name_node.children: - if prop.type == "shorthand_property_identifier_pattern": - sym_names.append(_read_text(prop, source)) - elif prop.type == "pair_pattern": - key = prop.child_by_field_name("key") - if key is not None: - sym_names.append(_read_text(key, source)) - elif value is not None and value.type == "member_expression": - # `const x = require('./m').y` — symbol is the property accessed - prop = value.child_by_field_name("property") - if prop is not None: - sym_names.append(_read_text(prop, source)) - if target_stem is not None: - for sym in sym_names: - edges.append({ - "source": file_nid, - "target": _make_id(target_stem, sym), - "relation": "imports", - "context": "import", - "confidence": "EXTRACTED", - "source_file": str_path, - "source_location": f"L{line}", - "weight": 1.0, - }) - return found - # Node types whose value is a callable, for the JS/TS assignment / class-field # / function-expression forms below. Older tree-sitter-javascript grammars # label a function expression `function`; current ones use `function_expression`. -_JS_FUNCTION_VALUE_TYPES = frozenset({"arrow_function", "function_expression", "function"}) -def _js_member_assignment_target(left, source: bytes): - """Classify the symbol an `assignment_expression` LHS defines when its RHS - is a function. Returns (kind, owner_name, member_name) or None. - - this.foo = fn → ("this", None, "foo") - exports.foo = fn → ("exports", None, "foo") - module.exports.foo = fn → ("exports", None, "foo") - Foo.prototype.bar = fn → ("prototype", "Foo", "bar") - - Any other shape (an arbitrary `obj.x = fn`) returns None and is skipped — - capturing those would reintroduce the bare-named / phantom-god-node class - of bug the module-level scope guard (#1077) exists to prevent. - """ - if left is None or left.type != "member_expression": - return None - prop = left.child_by_field_name("property") - if prop is None: - return None - member_name = _read_text(prop, source) - if not member_name: - return None - obj = left.child_by_field_name("object") - if obj is None: - return None - if obj.type == "this": - return ("this", None, member_name) - if obj.type == "identifier": - if _read_text(obj, source) == "exports": - return ("exports", None, member_name) - return None - if obj.type == "member_expression": - # module.exports.X or Foo.prototype.X - inner_obj = obj.child_by_field_name("object") - inner_prop = obj.child_by_field_name("property") - if inner_obj is None or inner_prop is None: - return None - inner_prop_name = _read_text(inner_prop, source) - if inner_obj.type == "identifier": - inner_obj_name = _read_text(inner_obj, source) - if inner_obj_name == "module" and inner_prop_name == "exports": - return ("exports", None, member_name) - if inner_prop_name == "prototype": - return ("prototype", inner_obj_name, member_name) - return None - - -def _js_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str, - nodes: list, edges: list, seen_ids: set, function_bodies: list, - parent_class_nid: str | None, add_node_fn, add_edge_fn) -> bool: - """Handle lexical_declaration (arrow functions, CJS requires, module-level const literals) for JS/TS. Returns True if handled.""" - # CommonJS / prototype member assignments whose value is a function: - # exports.X = () => {} → file-contained function X() - # module.exports.X = fn → file-contained function X() - # Foo.prototype.bar = fn → method bar() owned by Foo - # (`this.X = fn` lives inside a function body, which is not recursed here; - # it is captured at the enclosing function — see the function branch.) - if node.type == "expression_statement": - assign = next((c for c in node.children - if c.type == "assignment_expression"), None) - if assign is not None: - value = assign.child_by_field_name("right") - if value is not None and value.type in _JS_FUNCTION_VALUE_TYPES: - target = _js_member_assignment_target( - assign.child_by_field_name("left"), source) - if target is not None: - kind, owner_name, member_name = target - line = node.start_point[0] + 1 - handled = False - if kind == "exports": - nid = _make_id(stem, member_name) - add_node_fn(nid, f"{member_name}()", line) - add_edge_fn(file_nid, nid, "contains", line) - handled = True - elif kind == "prototype": - owner_nid = _make_id(stem, owner_name) - nid = _make_id(owner_nid, member_name) - add_node_fn(nid, f".{member_name}()", line) - add_edge_fn(owner_nid, nid, "method", line) - handled = True - if handled: - body = value.child_by_field_name("body") - if body: - function_bodies.append((nid, body)) - return True - - # Class fields whose value is a function: - # class C { handler = () => {} } → method handler() owned by C - # Reaches here with parent_class_nid set because class bodies are recursed - # with the class nid as parent. - if parent_class_nid and node.type in ("field_definition", "public_field_definition"): - prop = node.child_by_field_name("property") or node.child_by_field_name("name") - value = node.child_by_field_name("value") - if (prop is not None and value is not None - and value.type in _JS_FUNCTION_VALUE_TYPES): - field_name = _read_text(prop, source) - if field_name: - line = node.start_point[0] + 1 - nid = _make_id(parent_class_nid, field_name) - add_node_fn(nid, f".{field_name}()", line) - add_edge_fn(parent_class_nid, nid, "method", line) - body = value.child_by_field_name("body") - if body: - function_bodies.append((nid, body)) - return True - - if node.type in ("lexical_declaration", "variable_declaration"): - # CJS require imports — emit edges, do not block other lexical_declaration handling - require_found = _require_imports_js(node, source, file_nid, stem, edges, str_path) - - # Scope guard (#1077): only emit nodes for module-level declarations. - # Without this, `const x = ...` inside an arrow callback (e.g. inside - # `describe(() => { const set = new Set(...) })`) emits a bare-named - # node, and the same name collides across unrelated files producing - # phantom god-nodes. Bodies of arrow functions are walked separately - # via function_bodies, so we never need to emit nodes for locals here. - parent = node.parent - is_module_level = parent is not None and ( - parent.type == "program" - or (parent.type == "export_statement" - and parent.parent is not None - and parent.parent.type == "program") - ) - - # Arrow function declarations and module-level const literals (lexical_declaration only) - arrow_found = False - const_found = False - if node.type == "lexical_declaration" and is_module_level: - for child in node.children: - if child.type == "variable_declarator": - value = child.child_by_field_name("value") - if value and value.type in _JS_FUNCTION_VALUE_TYPES: - # `const f = () => {}` and `const f = function(){}` - name_node = child.child_by_field_name("name") - if name_node: - func_name = _read_text(name_node, source) - line = child.start_point[0] + 1 - func_nid = _make_id(stem, func_name) - add_node_fn(func_nid, f"{func_name}()", line) - add_edge_fn(file_nid, func_nid, "contains", line) - body = value.child_by_field_name("body") - if body: - function_bodies.append((func_nid, body)) - arrow_found = True - elif value and value.type in ( - "object", "array", "as_expression", "call_expression", "new_expression", - ): - # Module-level const with literal/object/array/factory value - name_node = child.child_by_field_name("name") - if name_node: - const_name = _read_text(name_node, source) - line = child.start_point[0] + 1 - const_nid = _make_id(stem, const_name) - add_node_fn(const_nid, const_name, line) - add_edge_fn(file_nid, const_nid, "contains", line) - const_found = True - if arrow_found: - return True - if const_found: - return True - if require_found: - return True - return False +# ── TS extra walk for namespace / module declarations ───────────────────────── # ── C# extra walk for namespace declarations ────────────────────────────────── -def _csharp_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str, - nodes: list, edges: list, seen_ids: set, function_bodies: list, - parent_class_nid: str | None, add_node_fn, add_edge_fn, - walk_fn) -> bool: - """Handle namespace_declaration for C#. Returns True if handled.""" - if node.type == "namespace_declaration": - name_node = node.child_by_field_name("name") - if name_node: - ns_name = _read_text(name_node, source) - ns_nid = _make_id(stem, ns_name) - line = node.start_point[0] + 1 - add_node_fn(ns_nid, ns_name, line) - add_edge_fn(file_nid, ns_nid, "contains", line) - body = node.child_by_field_name("body") - if body: - for child in body.children: - walk_fn(child, parent_class_nid) - return True - return False - # ── Swift extra walk for enum cases ────────────────────────────────────────── -def _swift_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str, - nodes: list, edges: list, seen_ids: set, function_bodies: list, - parent_class_nid: str | None, add_node_fn, add_edge_fn) -> bool: - """Handle enum_entry for Swift. Returns True if handled.""" - if node.type == "enum_entry" and parent_class_nid: - for child in node.children: - if child.type == "simple_identifier": - case_name = _read_text(child, source) - case_nid = _make_id(parent_class_nid, case_name) - line = node.start_point[0] + 1 - add_node_fn(case_nid, case_name, line) - add_edge_fn(parent_class_nid, case_nid, "case_of", line) - return True - return False + +# ── Java extra walk for enum constants ─────────────────────────────────────── # ── Language configs ────────────────────────────────────────────────────────── @@ -2141,13 +610,14 @@ def _swift_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: s _JS_CONFIG = LanguageConfig( ts_module="tree_sitter_javascript", class_types=frozenset({"class_declaration"}), - function_types=frozenset({"function_declaration", "method_definition"}), + function_types=frozenset({"function_declaration", "generator_function_declaration", "method_definition"}), import_types=frozenset({"import_statement", "export_statement"}), call_types=frozenset({"call_expression", "new_expression"}), call_function_field="function", call_accessor_node_types=frozenset({"member_expression"}), call_accessor_field="property", - function_boundary_types=frozenset({"function_declaration", "arrow_function", "method_definition"}), + call_accessor_object_field="object", + function_boundary_types=frozenset({"function_declaration", "generator_function_declaration", "arrow_function", "method_definition"}), import_handler=_import_js, ) @@ -2161,13 +631,14 @@ def _swift_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: s "enum_declaration", # named enums "type_alias_declaration", # named type aliases }), - function_types=frozenset({"function_declaration", "method_definition"}), + function_types=frozenset({"function_declaration", "generator_function_declaration", "method_definition", "method_signature"}), import_types=frozenset({"import_statement", "export_statement"}), call_types=frozenset({"call_expression", "new_expression"}), call_function_field="function", call_accessor_node_types=frozenset({"member_expression"}), call_accessor_field="property", - function_boundary_types=frozenset({"function_declaration", "arrow_function", "method_definition"}), + call_accessor_object_field="object", + function_boundary_types=frozenset({"function_declaration", "generator_function_declaration", "arrow_function", "method_definition"}), import_handler=_import_js, ) @@ -2185,6 +656,7 @@ def _swift_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: s call_function_field=_TS_CONFIG.call_function_field, call_accessor_node_types=_TS_CONFIG.call_accessor_node_types, call_accessor_field=_TS_CONFIG.call_accessor_field, + call_accessor_object_field=_TS_CONFIG.call_accessor_object_field, function_boundary_types=_TS_CONFIG.function_boundary_types, import_handler=_TS_CONFIG.import_handler, ) @@ -2251,7 +723,12 @@ def _swift_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: s _RUBY_CONFIG = LanguageConfig( ts_module="tree_sitter_ruby", - class_types=frozenset({"class"}), + # `module Foo` is a container node just like `class Foo` in tree-sitter's + # Ruby grammar (name in a `constant` child, body in `body_statement`), so it + # gets a node and its methods attach via `method` (#1640). Without it, plain + # utility/`module_function` modules produced no node and their methods hung + # off the file via `contains` with dot-less labels. + class_types=frozenset({"class", "module"}), function_types=frozenset({"method", "singleton_method"}), import_types=frozenset(), call_types=frozenset({"call"}), @@ -2296,7 +773,7 @@ def _swift_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: s # older forks use `simple_identifier`. Accept both so the extractor # works across grammar generations. name_fallback_child_types=("simple_identifier", "identifier"), - body_fallback_child_types=("function_body", "class_body"), + body_fallback_child_types=("function_body", "class_body", "enum_class_body"), function_boundary_types=frozenset({"function_declaration"}), import_handler=_import_kotlin, ) @@ -2337,45 +814,7 @@ def _swift_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: s ) -def _resolve_lua_import_target(raw_module: str, str_path: str) -> str: - """Resolve a Lua require() module name to a node id. - - Lua module names use dots as path separators: `require("pkg.b")` looks for - `pkg/b.lua` (or `pkg/b/init.lua`) relative to a package root. We probe the - importing file's directory and walk upward looking for a matching file on - disk; if found, the returned id matches the file node id `_extract_generic` - assigns to that file (`_make_id(str(path))`), so the edge lands on a real - node. When nothing matches, fall back to `_make_id` of the full dotted - module name so cross-file resolution can still complete via the symbol - resolution pass instead of dropping the edge entirely (#1075). - """ - if not raw_module: - return "" - rel = raw_module.replace(".", "/") - try: - start_dir = Path(str_path).parent - except Exception: - start_dir = None - if start_dir is not None: - probe = start_dir - # Walk up a few levels so requires from nested files still resolve when - # the package root is above the importing file. - for _ in range(6): - for suffix in (".lua", ".luau"): - cand = probe / f"{rel}{suffix}" - if cand.is_file(): - return _make_id(str(cand)) - for suffix in (".lua", ".luau"): - cand = probe / rel / f"init{suffix}" - if cand.is_file(): - return _make_id(str(cand)) - if probe.parent == probe: - break - probe = probe.parent - return _make_id(raw_module) - - -def _import_lua(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None: +def _import_lua(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str, scope_stack: list[str] | None = None) -> None: """Extract require('module') from Lua variable_declaration nodes.""" text = _read_text(node, source) import re @@ -2415,7 +854,7 @@ def _import_lua(node, source: bytes, file_nid: str, stem: str, edges: list, str_ ) -def _import_swift(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> list[tuple[str, str]]: +def _import_swift(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str, scope_stack: list[str] | None = None) -> list[tuple[str, str]]: """Emit module-level ``imports`` edges and report the imported modules. A Swift ``import CoreKit`` names a module, not a file path, so — unlike the @@ -2444,27 +883,6 @@ def _import_swift(node, source: bytes, file_nid: str, stem: str, edges: list, st return modules -def _read_csharp_type_name(node, source: bytes) -> str | None: - """Resolve a readable C# type name from a field/type node.""" - if node is None: - return None - if node.type in ("identifier", "predefined_type"): - return _read_text(node, source) - if node.type == "qualified_name": - return _read_text(node, source).split(".")[-1] - if node.type == "generic_name": - name_node = node.child_by_field_name("name") - if name_node is not None: - return _read_text(name_node, source) - for child in node.children: - if not child.is_named: - continue - name = _read_csharp_type_name(child, source) - if name: - return name - return None - - _SWIFT_CONFIG = LanguageConfig( ts_module="tree_sitter_swift", class_types=frozenset({"class_declaration", "protocol_declaration"}), @@ -2483,1633 +901,13 @@ def _read_csharp_type_name(node, source: bytes) -> str | None: # ── Ruby local type inference (for member-call resolution) ───────────────────── -def _ruby_new_class_name(node, source: bytes) -> str | None: - """Return ``ClassName`` if ``node`` is a ``ClassName.new(...)`` call, else None. - - Only a bare capitalized constant receiver counts (``Processor.new``); - namespaced (``A::B.new``) and dynamic receivers are intentionally ignored so - the binding stays unambiguous. - """ - if node is None or node.type != "call": - return None - recv = node.child_by_field_name("receiver") - meth = node.child_by_field_name("method") - if recv is None or meth is None: - return None - if recv.type != "constant" or _read_text(meth, source) != "new": - return None - return _read_text(recv, source) - - -def _ruby_local_class_bindings(body_node, source: bytes) -> dict[str, str | None]: - """Map ``local_var -> ClassName`` for ``var = ClassName.new`` within one Ruby - method body, not descending into nested method definitions. - - 100%-confidence contract: a variable assigned more than once, or to anything - other than a single ``Constant.new``, maps to ``None`` (ambiguous) so callers - never resolve it. Only the certain single-binding case carries a type. - """ - bindings: dict[str, str | None] = {} - boundary = {"method", "singleton_method"} - - def visit(n) -> None: - for child in n.children: - if child.type in boundary: - continue # nested method has its own scope - if child.type == "assignment": - left = child.child_by_field_name("left") - right = child.child_by_field_name("right") - if left is not None and left.type == "identifier": - var = _read_text(left, source) - cls = _ruby_new_class_name(right, source) if right is not None else None - if cls is None: - # assigned to something we can't type: poison if it was typed - if var in bindings: - bindings[var] = None - elif var in bindings: - if bindings[var] != cls: - bindings[var] = None # reassigned to a different class - else: - bindings[var] = cls - visit(child) - - visit(body_node) - return bindings +# `Const = (...)` shapes that define a lightweight class named after the +# constant. tree-sitter parses each as an `assignment`, not a `class`, so the +# generic class branch never saw them (#1640). # ── Generic extractor ───────────────────────────────────────────────────────── -def _extract_generic( - path: Path, config: LanguageConfig, *, source_override: bytes | None = None -) -> dict: - """Generic AST extractor driven by LanguageConfig. - - ``source_override`` parses the given bytes instead of reading ``path``, while - still keying nodes/edges off ``path``. Lets container formats (e.g. Vue SFCs) - mask the wrapper and parse just the embedded `` close tag - pos = m.end() - if lang is None: - lang_m = _VUE_SCRIPT_LANG_RE.search(m.group(1)) - if lang_m: - lang = lang_m.group(1).lower() - out.append(_blank(src[pos:])) - return "".join(out), lang def extract_vue(path: Path) -> dict: @@ -4766,4434 +1637,310 @@ def extract_csharp(path: Path) -> dict: return _extract_generic(path, _CSHARP_CONFIG) -def extract_apex(path: Path) -> dict: - """Extract classes, interfaces, enums, methods, and Salesforce constructs from - Apex .cls and .trigger files using regex (no tree-sitter grammar on PyPI).""" - import re as _re - try: - source = path.read_text(encoding="utf-8", errors="replace") - except OSError: - return {"nodes": [], "edges": []} +def extract_kotlin(path: Path) -> dict: + """Extract classes, objects, functions, and imports from a .kt/.kts file.""" + return _extract_generic(path, _KOTLIN_CONFIG) - str_path = str(path) - stem = _file_stem(path) - file_nid = _make_id(str_path) - nodes: list[dict] = [] - edges: list[dict] = [] - seen_ids: set[str] = set() +def extract_scala(path: Path) -> dict: + """Extract classes, objects, functions, and imports from a .scala file.""" + return _extract_generic(path, _SCALA_CONFIG) - def add_node(nid: str, label: str, line: int) -> None: - if nid not in seen_ids: - seen_ids.add(nid) - nodes.append({ - "id": nid, - "label": label, - "file_type": "code", - "source_file": str_path, - "source_location": f"L{line}", - }) - def add_edge(src: str, tgt: str, relation: str, line: int, - confidence: str = "EXTRACTED") -> None: - edges.append({ - "source": src, - "target": tgt, - "relation": relation, - "confidence": confidence, - "source_file": str_path, - "source_location": f"L{line}", - "weight": 1.0, - }) +def extract_php(path: Path) -> dict: + """Extract classes, functions, methods, namespace uses, and calls from a .php file.""" + return _extract_generic(path, _PHP_CONFIG) - add_node(file_nid, path.name, 1) - lines = source.splitlines() +# One level of balanced parens (e.g. `Foo #(Bar #(int))`) — bounded so malformed +# input cannot trigger pathological backtracking. - _ACCESS = r"(?:public|private|protected|global|webService)?" - _SHARING = r"(?:\s+(?:with|without|inherited)\s+sharing)?" - _MOD = r"(?:\s+(?:abstract|virtual|override|static|final|transient|testMethod))?" - _ANNOTATION = r"(?:\s*@\w+(?:\s*\([^)]*\))?\s*)*" - cls_re = _re.compile( - rf"^{_ANNOTATION}\s*{_ACCESS}{_SHARING}{_MOD}\s*class\s+(\w+)" - rf"(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s]+))?\s*\{{?", - _re.IGNORECASE, - ) - iface_re = _re.compile( - rf"^{_ANNOTATION}\s*{_ACCESS}{_SHARING}{_MOD}\s*interface\s+(\w+)" - rf"(?:\s+extends\s+([\w,\s]+))?\s*\{{?", - _re.IGNORECASE, - ) - enum_re = _re.compile( - rf"^{_ANNOTATION}\s*{_ACCESS}{_SHARING}{_MOD}\s*enum\s+(\w+)\s*\{{?", - _re.IGNORECASE, - ) - trigger_re = _re.compile( - r"^\s*trigger\s+(\w+)\s+on\s+(\w+)\s*\(", - _re.IGNORECASE, - ) - method_re = _re.compile( - rf"^{_ANNOTATION}\s*{_ACCESS}{_MOD}\s*(?:static\s+)?[\w<>\[\]]+\s+(\w+)\s*\([^)]*\)\s*(?:throws\s+\w+\s*)?\{{?", - _re.IGNORECASE, - ) - annotation_re = _re.compile(r"@(\w+)", _re.IGNORECASE) - soql_re = _re.compile(r"\[\s*SELECT\b[^\]]+FROM\s+(\w+)", _re.IGNORECASE) - dml_re = _re.compile(r"\b(insert|update|delete|upsert|merge|undelete)\s+\w", _re.IGNORECASE) - - _CONTROL_FLOW = frozenset({ - "if", "else", "for", "while", "do", "switch", "try", "catch", - "finally", "return", "throw", "new", "void", "null", - "true", "false", "this", "super", "class", "interface", "enum", - "trigger", "on", - }) +def extract_lua(path: Path) -> dict: + """Extract functions, methods, require() imports, and calls from a .lua file.""" + return _extract_generic(path, _LUA_CONFIG) - current_class_nid: str | None = None - pending_annotations: list[str] = [] - for lineno, line_text in enumerate(lines, start=1): - stripped = line_text.strip() +def extract_swift(path: Path) -> dict: + """Extract classes, structs, protocols, functions, imports, and calls from a .swift file.""" + return _extract_generic(path, _SWIFT_CONFIG) - if stripped.startswith("@"): - for m in annotation_re.finditer(stripped): - pending_annotations.append(m.group(1).lower()) - continue - tm = trigger_re.match(stripped) - if tm: - trig_name, sobject = tm.group(1), tm.group(2) - trig_nid = _make_id(stem, trig_name) - add_node(trig_nid, trig_name, lineno) - add_edge(file_nid, trig_nid, "contains", lineno) - sob_nid = _make_id(sobject) - if sob_nid not in seen_ids: - add_node(sob_nid, sobject, lineno) - add_edge(trig_nid, sob_nid, "uses", lineno, confidence="INFERRED") - current_class_nid = trig_nid - pending_annotations = [] - continue +# ── Julia extractor (custom walk) ──────────────────────────────────────────── - cm = cls_re.match(stripped) - if cm: - class_name = cm.group(1) - if class_name.lower() in _CONTROL_FLOW: - pending_annotations = [] - continue - class_nid = _make_id(stem, class_name) - add_node(class_nid, class_name, lineno) - add_edge(file_nid, class_nid, "contains", lineno) - if cm.group(2): - base = cm.group(2).strip() - base_nid = _make_id(stem, base) - if base_nid not in seen_ids: - base_nid = _make_id(base) - if base_nid not in seen_ids: - add_node(base_nid, base, lineno) - add_edge(class_nid, base_nid, "extends", lineno, confidence="INFERRED") - if cm.group(3): - for iface in cm.group(3).split(","): - iface = iface.strip() - if iface: - iface_nid = _make_id(stem, iface) - if iface_nid not in seen_ids: - iface_nid = _make_id(iface) - if iface_nid not in seen_ids: - add_node(iface_nid, iface, lineno) - add_edge(class_nid, iface_nid, "implements", lineno, confidence="INFERRED") - current_class_nid = class_nid - pending_annotations = [] - continue - im = iface_re.match(stripped) - if im: - iface_name = im.group(1) - if iface_name.lower() in _CONTROL_FLOW: - pending_annotations = [] - continue - iface_nid = _make_id(stem, iface_name) - add_node(iface_nid, iface_name, lineno) - add_edge(file_nid if current_class_nid is None else current_class_nid, - iface_nid, "contains", lineno) - pending_annotations = [] - continue +# ── Go extractor (custom walk) ──────────────────────────────────────────────── - em = enum_re.match(stripped) - if em: - enum_name = em.group(1) - if enum_name.lower() in _CONTROL_FLOW: - pending_annotations = [] - continue - enum_nid = _make_id(stem, enum_name) - add_node(enum_nid, enum_name, lineno) - add_edge(file_nid if current_class_nid is None else current_class_nid, - enum_nid, "contains", lineno) - pending_annotations = [] - continue - if current_class_nid is not None: - mm = method_re.match(stripped) - if mm: - method_name = mm.group(1) - if method_name.lower() not in _CONTROL_FLOW: - method_nid = _make_id(current_class_nid, method_name) - method_label = f".{method_name}()" - add_node(method_nid, method_label, lineno) - add_edge(current_class_nid, method_nid, "method", lineno) - if "auraenabled" in pending_annotations or "invocablemethod" in pending_annotations: - add_edge(file_nid, method_nid, "contains", lineno, confidence="INFERRED") - pending_annotations = [] - continue +# ── Rust extractor (custom walk) ────────────────────────────────────────────── - pending_annotations = [] +# Common Rust trait/stdlib method names that appear in virtually every codebase. +# Resolving these cross-file produces spurious INFERRED edges across crate +# boundaries (issue #908) — skip them from the unresolved-call queue entirely. - for sm in soql_re.finditer(line_text): - sobject = sm.group(1) - sob_nid = _make_id(sobject) - if sob_nid not in seen_ids: - add_node(sob_nid, sobject, lineno) - src = current_class_nid or file_nid - add_edge(src, sob_nid, "uses", lineno, confidence="INFERRED") - for dm in dml_re.finditer(line_text): - dml_op = dm.group(1).lower() - dml_nid = _make_id(f"dml_{dml_op}") - if dml_nid not in seen_ids: - add_node(dml_nid, dml_op, lineno) - src = current_class_nid or file_nid - add_edge(src, dml_nid, "uses", lineno, confidence="INFERRED") +# ── Zig ─────────────────────────────────────────────────────────────────────── - return {"nodes": nodes, "edges": edges} +# ── PowerShell ──────────────────────────────────────────────────────────────── -def extract_kotlin(path: Path) -> dict: - """Extract classes, objects, functions, and imports from a .kt/.kts file.""" - return _extract_generic(path, _KOTLIN_CONFIG) +# ── PowerShell manifest (.psd1) ────────────────────────────────────────────── -def extract_scala(path: Path) -> dict: - """Extract classes, objects, functions, and imports from a .scala file.""" - return _extract_generic(path, _SCALA_CONFIG) +# Keys in a .psd1 whose values are module names/paths we treat as imports. -def extract_php(path: Path) -> dict: - """Extract classes, functions, methods, namespace uses, and calls from a .php file.""" - return _extract_generic(path, _PHP_CONFIG) +# ── Cross-file import resolution ────────────────────────────────────────────── +def _canonicalize_csharp_namespace_nodes(all_nodes: list[dict], all_edges: list[dict]) -> None: + """Collapse duplicate C# namespace node entries to one canonical node per label.""" + by_label: dict[str, list[dict]] = {} + for node in all_nodes: + if node.get("type") != "namespace": + continue + label = node.get("label") + if isinstance(label, str): + by_label.setdefault(label, []).append(node) + remap: dict[str, str] = {} + drop_node_ids: set[int] = set() + for group in by_label.values(): + if len(group) < 2: + continue + canonical = sorted( + group, + key=lambda node: ( + str(node.get("source_file") or ""), + str(node.get("source_location") or ""), + str(node.get("id") or ""), + ), + )[0] + canonical_id = canonical.get("id") + for node in group: + if node is canonical: + continue + drop_node_ids.add(id(node)) + dup_id = node.get("id") + if isinstance(dup_id, str) and isinstance(canonical_id, str): + remap[dup_id] = canonical_id + + if remap: + for edge in all_edges: + if edge.get("source") in remap: + edge["source"] = remap[str(edge["source"])] + if edge.get("target") in remap: + edge["target"] = remap[str(edge["target"])] + + if drop_node_ids: + all_nodes[:] = [node for node in all_nodes if id(node) not in drop_node_ids] + + +# Languages whose identifiers are case-insensitive, so cross-file name resolution +# may fold case. Everywhere else, case is semantic (`Path` the class vs `PATH` the +# env var are distinct) and folding manufactures false edges / super-hubs (#1581). +_CASE_INSENSITIVE_EXTS = frozenset({ + ".php", ".phtml", ".php3", ".php4", ".php5", ".php7", ".phps", # PHP fns/classes + ".sql", # SQL identifiers + ".nim", ".nims", ".nimble", # Nim (style-insensitive) +}) -def extract_dart(path: Path) -> dict: - """Extract classes, mixins, functions, imports, generic calls, and annotations from a .dart file using regex.""" - try: - src = path.read_text(encoding="utf-8", errors="replace") - except OSError: - return {"error": f"cannot read {path}"} - - # Remove inline and multi-line comments while leaving string literals untouched to prevent stripping URLs/paths inside strings - comment_string_pattern = re.compile( - r'"""(?:\\.|[\s\S])*?"""' - r"|'''(?:\\.|[\s\S])*?'''" - r'|"(?:\\.|[^"\\])*"' - r"|'(?:\\.|[^'\\])*'" - r"|/\*[\s\S]*?\*/" - r"|//[^\n]*" - ) - def _comment_replace(match: re.Match) -> str: - matched_text = match.group(0) - if matched_text.startswith("/"): - return "" - return matched_text - src_clean = comment_string_pattern.sub(_comment_replace, src) - stem = _file_stem(path) - file_nid = _make_id(str(path)) +def _lang_is_case_insensitive(source_file: object) -> bool: + """True when the file's language resolves identifiers case-insensitively (#1581).""" + if not source_file: + return False + return Path(str(source_file)).suffix.lower() in _CASE_INSENSITIVE_EXTS + + +# Language interop families for cross-file call resolution. A call in one language +# can never bind by name to a definition in another family — a TSX component does +# not invoke a Kotlin method, and a Python function does not invoke a Java one. +# Families are grouped by REAL interop so legitimate cross-language resolution +# keeps working: Kotlin/Java/Scala/Groovy share the JVM, C/C++/Objective-C/CUDA +# share headers and symbols (Swift bridges to Objective-C), and JS/TS variants +# (plus Vue/Svelte/Astro SFC script blocks) compile into one module graph. +# Extensions absent from this map (docs, configs, unknown languages) resolve to +# no family and are never filtered — same permissive default as before. +_LANG_FAMILY_BY_EXT: dict[str, str] = { + # JS/TS module graph (SFCs embed JS/TS) + ".js": "jsts", ".jsx": "jsts", ".mjs": "jsts", ".cjs": "jsts", + ".ts": "jsts", ".tsx": "jsts", ".mts": "jsts", ".cts": "jsts", + ".vue": "jsts", ".svelte": "jsts", ".astro": "jsts", + # JVM interop + ".java": "jvm", ".kt": "jvm", ".kts": "jvm", + ".scala": "jvm", ".groovy": "jvm", ".gradle": "jvm", + # C-family: shared headers, Objective-C/C++ mix, Swift↔ObjC bridging + ".c": "native", ".h": "native", ".cpp": "native", ".cc": "native", + ".cxx": "native", ".hpp": "native", ".cu": "native", ".cuh": "native", + ".metal": "native", ".m": "native", ".mm": "native", ".swift": "native", + # Single-language families + ".py": "python", + ".go": "go", + ".rs": "rust", + ".rb": "ruby", ".rake": "ruby", + ".php": "php", ".phtml": "php", ".php3": "php", ".php4": "php", + ".php5": "php", ".php7": "php", ".phps": "php", + ".cs": "dotnet", ".razor": "dotnet", ".cshtml": "dotnet", ".xaml": "dotnet", + ".lua": "lua", ".luau": "lua", + ".zig": "zig", + ".ex": "elixir", ".exs": "elixir", + ".jl": "julia", + ".dart": "dart", + ".sh": "shell", ".bash": "shell", + ".ps1": "powershell", ".psm1": "powershell", ".psd1": "powershell", +} - # Check if this is a part-of file and redirect to parent - part_of_match = re.search(r"^\s*part\s+of\s+['\"]([^'\"]+)['\"]", src_clean, re.MULTILINE) - is_part = False - if part_of_match: - parent_ref = part_of_match.group(1) - if parent_ref.endswith(".dart"): - try: - parent_path = (path.parent / parent_ref).resolve() - if parent_path.exists(): - stem = _file_stem(parent_path) - file_nid = _make_id(str(parent_path)) - is_part = True - except Exception: - pass - nodes = [] - if not is_part: - nodes.append({"id": file_nid, "label": path.name, "file_type": "code", - "source_file": str(path), "source_location": None}) - edges = [] - defined: set[str] = set() - - def add_node(nid: str, label: str, ftype: str = "code", source_file: str | None = str(path)) -> None: - if nid not in defined: - nodes.append({"id": nid, "label": label, "file_type": ftype, - "source_file": source_file, "source_location": None}) - defined.add(nid) - - def add_edge(src_id: str, tgt_id: str, relation: str, weight: float = 1.0, context: str | None = None) -> None: - edge = {"source": src_id, "target": tgt_id, "relation": relation, - "confidence": "EXTRACTED", "confidence_score": 1.0, - "source_file": str(path), "source_location": None, "weight": weight} - if context: - edge["context"] = context - edges.append(edge) +def _lang_family(source_file: object) -> str | None: + """Interop family of the file's language, or None when unknown/not code.""" + if not source_file: + return None + return _LANG_FAMILY_BY_EXT.get(Path(str(source_file)).suffix.lower()) - def _split_types(text: str) -> list[str]: - parts = [] - current = [] - depth = 0 - for char in text: - if char == "<": - depth += 1 - current.append(char) - elif char == ">": - depth -= 1 - current.append(char) - elif char == "," and depth == 0: - parts.append("".join(current).strip()) - current = [] - else: - current.append(char) - if current: - parts.append("".join(current).strip()) - return [p for p in parts if p] - - def _find_matching_brace(text: str, start_pos: int) -> int: - brace_count = 0 - in_double_quote = False - in_single_quote = False - escape = False - - first_brace = text.find("{", start_pos) - if first_brace == -1: - return len(text) - - brace_count = 1 - i = first_brace + 1 - n = len(text) - while i < n: - char = text[i] - if escape: - escape = False - i += 1 - continue - if char == "\\": - escape = True - i += 1 - continue - if text[i:i+3] == '"""' and not in_single_quote: - i += 3 - end = text.find('"""', i) - i = end + 3 if end != -1 else n - continue - if text[i:i+3] == "'''" and not in_double_quote: - i += 3 - end = text.find("'''", i) - i = end + 3 if end != -1 else n - continue - if char == '"' and not in_single_quote: - in_double_quote = not in_double_quote - elif char == "'" and not in_double_quote: - in_single_quote = not in_single_quote - elif not in_double_quote and not in_single_quote: - if char == "{": - brace_count += 1 - elif char == "}": - brace_count -= 1 - if brace_count == 0: - return i + 1 - i += 1 - return len(text) - - # 1. Classes, mixins, and enums declarations (with inheritance, mixins, interfaces, and generics) - # Supports multiple combined modifiers (e.g., abstract base class, mixin class) without capturing "class" as a name - class_pattern = r"^\s*(?:(?:abstract|sealed|base|interface|final|mixin)\s+)*(?:class|mixin|enum|extension\s+type)\s+(\w+)" - for m in re.finditer(class_pattern, src_clean, re.MULTILINE): - class_name = m.group(1) - class_nid = _make_id(stem, class_name) - add_node(class_nid, class_name) - add_edge(file_nid, class_nid, "defines") - - # Manually parse extends/on, with, and implements in header to handle nested generics brackets balanced - start_idx = m.end() - rest = src_clean[start_idx : start_idx + 500] - - # Skip class generic parameters - if rest.lstrip().startswith("<"): - offset = rest.find("<") - depth = 1 - i = offset + 1 - while i < len(rest) and depth > 0: - if rest[i] == "<": depth += 1 - elif rest[i] == ">": depth -= 1 - i += 1 - rest = rest[i:] - - # Skip primary constructor (e.g. extension type MyExt(int id)) - if rest.lstrip().startswith("("): - offset = rest.find("(") - depth = 1 - i = offset + 1 - while i < len(rest) and depth > 0: - if rest[i] == "(": depth += 1 - elif rest[i] == ")": depth -= 1 - i += 1 - rest = rest[i:] - - header_end = rest.find("{") - if header_end == -1: - header_end = rest.find(";") - if header_end == -1: - header_end = len(rest) - header = rest[:header_end] - - base_class = None - generics = None - mixins_list = [] - interfaces_list = [] - - # Parse extends or on - extends_m = re.search(r"^\s*(?:extends|on)\s+([a-zA-Z0-9_.]+)", header) - if extends_m: - base_class = extends_m.group(1) - rest_header = header[extends_m.end():] - if rest_header.strip().startswith("<"): - start_idx = rest_header.find("<") - depth = 1 - i = start_idx + 1 - while i < len(rest_header) and depth > 0: - if rest_header[i] == "<": - depth += 1 - elif rest_header[i] == ">": - depth -= 1 - if depth == 0: - generics = rest_header[start_idx + 1 : i] - break - i += 1 - if generics is not None: - header = rest_header[i + 1:] - else: - header = rest_header - else: - header = rest_header - - # Parse with - with_m = re.search(r"^\s*with\s+", header) - if with_m: - rest_header = header[with_m.end():] - impl_idx = rest_header.find("implements") - if impl_idx != -1: - mixins_str = rest_header[:impl_idx] - header = rest_header[impl_idx:] - else: - mixins_str = rest_header - header = "" - mixins_list = _split_types(mixins_str) - - # Parse implements - impl_m = re.search(r"^\s*implements\s+", header) - if impl_m: - interfaces_list = _split_types(header[impl_m.end():]) - - # Map extends inheritance relation - if base_class: - base_nid = _make_id(base_class) - add_node(base_nid, base_class, source_file=None) - add_edge(class_nid, base_nid, "inherits") - - # Map generic type arguments (e.g. MyBloc extends Bloc) - if generics: - for gen in _split_types(generics): - gen_clean = gen.split("<")[0].strip() - if gen_clean not in {"String", "int", "double", "bool", "num", "dynamic", "Object", "void"}: - gen_nid = _make_id(gen_clean) - add_node(gen_nid, gen_clean, source_file=None) - add_edge(class_nid, gen_nid, "references") - - # Map mixins - for mixin in mixins_list: - mixin_clean = mixin.split("<")[0].strip() - mixin_nid = _make_id(mixin_clean) - add_node(mixin_nid, mixin_clean, source_file=None) - add_edge(class_nid, mixin_nid, "mixes_in") - - # Map interfaces - for interface in interfaces_list: - interface_clean = interface.split("<")[0].strip() - interface_nid = _make_id(interface_clean) - add_node(interface_nid, interface_clean, source_file=None) - add_edge(class_nid, interface_nid, "implements") - - # Extract class body for precise framework dependencies and event handling - start_idx = m.start() - brace_pos = src_clean.find("{", start_idx) - semi_pos = src_clean.find(";", start_idx) - - has_body = brace_pos != -1 - if has_body and semi_pos != -1 and semi_pos < brace_pos: - has_body = False - - if has_body: - end_pos = _find_matching_brace(src_clean, start_idx) - class_body = src_clean[brace_pos:end_pos] - - # Bloc event registration: on() - for em in re.finditer(r"\bon<(\w+)>\s*\(", class_body): - event_name = em.group(1) - event_nid = _make_id(event_name) - add_node(event_nid, event_name, source_file=None) - add_edge(class_nid, event_nid, "calls", context="bloc_event") - - # Bloc state emissions: emit(MyState) or yield MyState - for sm in re.finditer(r"\b(?:emit|yield)\s*\(?\s*(?:const\s+)?([A-Z]\w*)\b", class_body): - state_name = sm.group(1) - if state_name not in {"String", "List", "Map", "Set", "Future", "Stream", "Object"}: - state_nid = _make_id(state_name) - add_node(state_nid, state_name, source_file=None) - add_edge(class_nid, state_nid, "calls", context="emit_state") - - # Bloc event additions: widget.add(MyEvent()) or bloc.add(MyEvent()) - for am in re.finditer(r"\b(?:\w*[Bb]loc\w*|context\.read<\w+>\(\))\.add\(\s*(?:const\s+)?([A-Z]\w*)\b", class_body): - event_name = am.group(1) - if event_name not in {"String", "List", "Map", "Set", "Future", "Stream", "Object"}: - event_nid = _make_id(event_name) - add_node(event_nid, event_name, source_file=None) - add_edge(class_nid, event_nid, "calls", context="bloc_add_event") - - # Riverpod provider references: ref.watch(provider) - for rm in re.finditer(r"\bref\.(?:watch|read|listen)\s*\(\s*(\w+)\b", class_body): - provider_name = rm.group(1) - provider_nid = _make_id(provider_name) - add_node(provider_nid, provider_name, source_file=None) - add_edge(class_nid, provider_nid, "references", context="riverpod_reference") - - # Widget to Bloc references: BlocBuilder - for bm in re.finditer(r"\bBloc(?:Builder|Listener|Consumer|Provider|Selector)\s*<\s*([a-zA-Z0-9_]+)\b", class_body): - bloc_name = bm.group(1) - if bloc_name not in {"String", "int", "double", "bool", "num", "dynamic", "Object", "void"}: - bloc_nid = _make_id(bloc_name) - add_node(bloc_nid, bloc_name, source_file=None) - add_edge(class_nid, bloc_nid, "references", context="bloc_widget_binding") - - # context.read() or BlocProvider.of(context) - for lm in re.finditer(r"\b(?:read|watch|select|of)\s*<([a-zA-Z0-9_]+)>", class_body): - bloc_name = lm.group(1) - if bloc_name not in {"String", "int", "double", "bool", "num", "dynamic", "Object", "void"}: - bloc_nid = _make_id(bloc_name) - add_node(bloc_nid, bloc_name, source_file=None) - add_edge(class_nid, bloc_nid, "references", context="bloc_lookup") - - # 2. Annotations mapping (class, mixin, enum, or function level annotations) - # Support: @riverpod, @Riverpod(...), @injectable, @singleton, @RoutePage(), @HiveType(typeId: 0), @RestApi() - # Matches `@annotation` and links it to the next class/mixin/enum/function declaration in the file - annotation_pattern = r"@(\w+)(?:\([^)]*\))?" - for am in re.finditer(annotation_pattern, src_clean): - annotation_name = am.group(1) - if annotation_name in {"override", "deprecated", "required", "protected", "mustCallSuper"}: - continue - annotation_pos = am.end() - intervening_text = src_clean[annotation_pos : annotation_pos + 300] - class_m = re.search(r"^\s*(?:(?:abstract|sealed|base|interface|final|mixin)\s+)*(?:class|mixin|enum|extension\s+type)\s+(\w+)", intervening_text, re.MULTILINE) - func_m = re.search(r"^\s*(?:factory\s+|static\s+|async\s+|external\s+|abstract\s+)?(?:\([^)]+\)|[a-zA-Z0-9_<>,.?]+)(?:\s+[a-zA-Z0-9_<>,.?]+){0,3}\s+(\w+)\s*\(", intervening_text, re.MULTILINE) +def _node_label_key(node: dict, fold: bool = False) -> str: + label = str(node.get("label", "")).strip() + key = re.sub(r"[^a-zA-Z0-9]+", "", label) + return key.lower() if fold else key - target_nid = None - target_name = None - target_type = None - if class_m and func_m: - if class_m.start() < func_m.start(): - target_name = class_m.group(1) - target_type = "class" - target_nid = _make_id(stem, target_name) - else: - target_name = func_m.group(1) - target_type = "function" - target_nid = _make_id(stem, target_name) - elif class_m: - target_name = class_m.group(1) - target_type = "class" - target_nid = _make_id(stem, target_name) - elif func_m: - target_name = func_m.group(1) - target_type = "function" - target_nid = _make_id(stem, target_name) - - if target_nid and target_name: - actual_intervening = intervening_text[:min(class_m.start() if class_m else 300, func_m.start() if func_m else 300)] - if ";" not in actual_intervening and "}" not in actual_intervening and "{" not in actual_intervening: - annotation_nid = _make_id("annotation", annotation_name.lower()) - add_node(annotation_nid, f"@{annotation_name}", ftype="concept", source_file=None) - add_edge(target_nid, annotation_nid, "configures") - - # Riverpod specific provider generation mapping (supports camelCase class and functional providers) - if annotation_name.lower() == "riverpod": - if target_type == "class": - provider_name = target_name[0].lower() + target_name[1:] + "Provider" if len(target_name) > 1 else target_name.lower() + "Provider" - else: - provider_name = target_name + "Provider" - provider_nid = _make_id(provider_name) - add_node(provider_nid, provider_name, ftype="concept", source_file=str(path)) - add_edge(target_nid, provider_nid, "defines", context="riverpod_provider") - - # 2.5 Typedefs (Type Aliases) - typedef_pattern = r"^\s*typedef\s+(\w+)\s*(?:<[^>]+>)?\s*=\s*([a-zA-Z0-9_<>,.?\s]+);" - for m in re.finditer(typedef_pattern, src_clean, re.MULTILINE): - typedef_name = m.group(1) - target_type = m.group(2).split("<")[0].split(".")[-1].strip() - if target_type not in {"String", "int", "double", "bool", "num", "dynamic", "Object", "List", "Map", "Set", "void", "Function"}: - typedef_nid = _make_id(stem, typedef_name) - add_node(typedef_nid, typedef_name) - add_edge(file_nid, typedef_nid, "defines") - target_nid = _make_id(target_type) - add_node(target_nid, target_type, source_file=None) - add_edge(typedef_nid, target_nid, "references", context="typedef") - - # 3. Extensions (extension MyExt on MyClass) - ext_pattern = r"^\s{0,4}extension\s+(\w+)?(?:<[^>]+>)?\s+on\s+(\w+)" - for m in re.finditer(ext_pattern, src_clean, re.MULTILINE): - ext_name = m.group(1) or f"{stem}_anonymous_extension" - target_class = m.group(2) - - ext_nid = _make_id(stem, ext_name) - label = m.group(1) or f"Extension on {target_class}" - add_node(ext_nid, label) - add_edge(file_nid, ext_nid, "defines") - - target_nid = _make_id(target_class) - add_node(target_nid, target_class, source_file=None) - add_edge(ext_nid, target_nid, "extends") - - # 4. Top-level and class-level variable declarations (generic variables, records, late, and destructuring) - # Restrict indentation to 0-2 spaces to avoid matching local variables inside functions or switch expressions - var_pattern = r"^\s{0,2}(?:late\s+)?(?:(?:final|const|var)\s+)?(?:\([^)]+\)\s+|([a-zA-Z0-9_<>,.?]+(?:\s+[a-zA-Z0-9_<>,.?]+){0,3})\s+)?(?:(\w+)|(?:\w+\s*)?\(([^)]+)\))\s*(?:=|$|;)" - for m in re.finditer(var_pattern, src_clean, re.MULTILINE): - var_type = m.group(1) - single_name = m.group(2) - destructured_names = m.group(3) - - if not re.match(r"^\s*(?:late|final|const|var)\b", m.group(0)) and not var_type: - continue +def _is_top_level_function_definition(node: dict) -> bool: + """A free/top-level function def (label ``name()``), not a method or type. + + Methods carry a leading dot (``.foo()``) or a qualifier (``Class.foo()``); + excluding those keeps a bare-name reference from binding to a receiver-scoped + method, which the receiver-typed resolvers own (#1781). + """ + label = str(node.get("label", "")).strip() + return ( + node.get("file_type") == "code" + and label.endswith(")") + and not label.startswith(".") + and "." not in label + ) + - if single_name: - if single_name not in {"if", "for", "while", "switch", "catch", "return"}: - var_nid = _make_id(stem, single_name) - add_node(var_nid, single_name) - add_edge(file_nid, var_nid, "defines") - - if var_type and var_type not in {"String", "int", "double", "bool", "num", "dynamic", "Object", "List", "Map", "Set", "void"}: - clean_type = var_type.split("<")[0].split(".")[-1].strip() - type_nid = _make_id(clean_type) - add_node(type_nid, clean_type, source_file=None) - add_edge(file_nid, type_nid, "references", context="variable_type") - elif destructured_names: - for name in [n.strip() for n in destructured_names.split(",") if n.strip()]: - if ":" in name: - name = name.split(":")[-1].strip() - if re.match(r"^[a-zA-Z_]\w*$", name) and not re.match(r"^[A-Z]", name): - if name not in {"if", "for", "while", "switch", "catch", "return"}: - var_nid = _make_id(stem, name) - add_node(var_nid, name) - add_edge(file_nid, var_nid, "defines") - - # 5. Top-level and member functions/methods (supports typed/generic/record return types and Riverpod/Bloc references) - # Restrict indentation to 0-2 spaces to avoid matching nested local functions or methods inside multiline switch statements - method_pattern = r"^\s{0,2}(?:factory\s+|static\s+|async\s+|external\s+|abstract\s+)?(?:\([^)]+\)|[a-zA-Z0-9_<>,.?]+)(?:\s+[a-zA-Z0-9_<>,.?]+){0,3}\s+(\w+(?:\.\w+)?)\s*\(" - for m in re.finditer(method_pattern, src_clean, re.MULTILINE): - raw_name = m.group(1) - name = raw_name.split(".")[-1] - if name in {"if", "for", "while", "switch", "catch", "return", "void", "dynamic", "final", "const", "get", "set"}: +def _rewire_unique_stub_nodes(nodes: list[dict], edges: list[dict]) -> None: + """Map unresolved no-source stubs to a unique real definition with the same label.""" + real_by_label: dict[str, list[dict]] = {} # exact-case type-like (all languages) + real_by_label_ci: dict[str, list[dict]] = {} # case-INSENSITIVE-language reals only + func_by_label: dict[str, list[dict]] = {} # top-level function defs (#1781) + stubs: list[dict] = [] + + for node in nodes: + key = _node_label_key(node) + if not key: continue - if re.match(r"^[A-Z]", name): + if node.get("source_file"): + if _is_type_like_definition(node): + # Match stubs case-SENSITIVELY: a `Path` reference must not rewire to a + # `PATH` env var (#1581). Fold only for genuinely case-insensitive + # languages, where `foo` legitimately resolves to `Foo`. + real_by_label.setdefault(key, []).append(node) + if _lang_is_case_insensitive(node.get("source_file")): + real_by_label_ci.setdefault( + _node_label_key(node, fold=True), []).append(node) + elif _is_top_level_function_definition(node): + func_by_label.setdefault(key, []).append(node) continue - nid = _make_id(stem, name) - add_node(nid, name) - add_edge(file_nid, nid, "defines") - - # Get function body using matching brace to extract Riverpod reference patterns - start_idx = m.start() - brace_pos = src_clean.find("{", start_idx) - semi_pos = src_clean.find(";", start_idx) - arrow_pos = src_clean.find("=>", start_idx) - - has_body = brace_pos != -1 - if has_body and semi_pos != -1 and semi_pos < brace_pos: - has_body = False - if has_body and arrow_pos != -1 and arrow_pos < brace_pos: - has_body = False - - if has_body: - end_pos = _find_matching_brace(src_clean, start_idx) - func_body = src_clean[brace_pos:end_pos] - - # Extract Riverpod provider references: ref.watch(provider) - for rm in re.finditer(r"\bref\.(?:watch|read|listen)\s*\(\s*(\w+)\b", func_body): - provider_name = rm.group(1) - provider_nid = _make_id(provider_name) - add_node(provider_nid, provider_name, source_file=None) - add_edge(nid, provider_nid, "references", context="riverpod_reference") - - # Extract Bloc event additions: widget.add(MyEvent()) or bloc.add(MyEvent()) - for am in re.finditer(r"\b(?:\w*[Bb]loc\w*|context\.read<\w+>\(\))\.add\(\s*(?:const\s+)?([A-Z]\w*)\b", func_body): - event_name = am.group(1) - if event_name not in {"String", "List", "Map", "Set", "Future", "Stream", "Object"}: - event_nid = _make_id(event_name) - add_node(event_nid, event_name, source_file=None) - add_edge(nid, event_nid, "calls", context="bloc_add_event") - - # context.read() or BlocProvider.of(context) - for lm in re.finditer(r"\b(?:read|watch|select|of)\s*<([a-zA-Z0-9_]+)>", func_body): - bloc_name = lm.group(1) - if bloc_name not in {"String", "int", "double", "bool", "num", "dynamic", "Object", "void"}: - bloc_nid = _make_id(bloc_name) - add_node(bloc_nid, bloc_name, source_file=None) - add_edge(nid, bloc_nid, "references", context="bloc_lookup") - - # Universal Navigation Patters (GoRouter, AutoRoute, Navigator) - for nm in re.finditer(r"\b(?:go|push|goNamed|pushNamed|replace|replaceNamed)\s*\(\s*(?:context\s*,\s*)?['\"]([a-zA-Z0-9_/?=&%-]+)['\"]", func_body): - route_path = nm.group(1) - route_nid = _make_id("route", route_path.replace("/", "_").replace("?", "_").replace("=", "_").replace("&", "_")) - add_node(route_nid, f"Route {route_path}", ftype="concept", source_file=None) - add_edge(nid, route_nid, "navigates", context="route_path") - - for cm in re.finditer(r"\b(?:go|push|goNamed|pushNamed|replace|replaceNamed)\s*\(\s*(?:context\s*,\s*)?([A-Z][a-zA-Z0-9_]*\.[a-zA-Z0-9_]+)", func_body): - route_const = cm.group(1) - route_nid = _make_id("route", route_const.replace(".", "_")) - add_node(route_nid, route_const, ftype="concept", source_file=None) - add_edge(nid, route_nid, "navigates", context="route_const") - - for om in re.finditer(r"\b(?:push|replace)\s*\(\s*(?:context\s*,\s*)?.*?\b([A-Z]\w*(?:Route|Screen|Page))\b", func_body): - route_class = om.group(1) - route_nid = _make_id(route_class) - add_node(route_nid, route_class, source_file=None) - add_edge(nid, route_nid, "navigates", context="route_object") - - # 6. Imports and Exports - for m in re.finditer(r"""^\s*import\s+['"]([^'"]+)['"]""", src_clean, re.MULTILINE): - pkg = m.group(1) - tgt_nid = _make_id(pkg) - add_node(tgt_nid, pkg, source_file=None) - add_edge(file_nid, tgt_nid, "imports") - - for m in re.finditer(r"""^\s*export\s+['"]([^'"]+)['"]""", src_clean, re.MULTILINE): - pkg = m.group(1) - tgt_nid = _make_id(pkg) - add_node(tgt_nid, pkg, source_file=None) - add_edge(file_nid, tgt_nid, "exports") - - # 7. Generic Invocations / Type Lookups (Universal Dependency Lookup) - # Matches any method call with type parameters: methodName() or object.methodName() - # Automatically extracts GetIt, Injectable, Riverpod, Provider, BlocProvider, and InheritedWidget type lookups! - generic_call_pattern = r"\b\w+<([a-zA-Z0-9_.]+(?:<[a-zA-Z0-9_.,\s<>]+>)?)\s*>\s*\(" - type_blacklist = {"String", "int", "double", "bool", "num", "dynamic", "Object", "List", "Map", "Set", "Future", "Stream", "void"} - for m in re.finditer(generic_call_pattern, src_clean): - type_name = m.group(1).split(".")[-1].strip() - clean_name = type_name.split("<")[0].strip() - if clean_name not in type_blacklist: - target_nid = _make_id(clean_name) - add_node(target_nid, clean_name, source_file=None) - add_edge(file_nid, target_nid, "references", context="type_lookup") + stubs.append(node) - return {"nodes": nodes, "edges": edges} + # Language families referencing each stub, for the function-merge guard (#1781): + # a cross-module `references` edge to a function used to dangle on a sourceless + # name-only stub because functions were excluded as rewire targets. We now allow + # a UNIQUE function definition to absorb it, but only when it shares a language + # family with the stub's referrers — so a Python `get_db` reference can't bind to + # a unique Go `get_db()` (mirrors the #1718/#1749 interop guard). + stub_ids = {str(s.get("id")) for s in stubs if s.get("id")} + stub_families: dict[str, set] = {} + supertype_stub_ids: set[str] = set() # stubs used as a base type — never a function + _SUPERTYPE_RELATIONS = {"inherits", "implements", "extends"} + for edge in edges: + rel = edge.get("relation") + for endpoint in ("source", "target"): + nid = edge.get(endpoint) + if nid in stub_ids: + fam = _lang_family(edge.get("source_file")) + if fam is not None: + stub_families.setdefault(str(nid), set()).add(fam) + # A stub referenced as a supertype must resolve to a class/type, + # not a same-named function (you don't inherit from a function). + if endpoint == "target" and rel in _SUPERTYPE_RELATIONS: + supertype_stub_ids.add(str(nid)) + remap: dict[str, str] = {} + for stub in stubs: + stub_id = str(stub.get("id", "")) + if not stub_id: + continue + candidates = real_by_label.get(_node_label_key(stub), []) + if len(candidates) != 1: + # No unique exact type match — fall back to a case-insensitive match, but + # only against case-insensitive-language definitions (so a case-sensitive + # `PATH` can never absorb a `Path` reference). + candidates = real_by_label_ci.get(_node_label_key(stub, fold=True), []) + if len(candidates) != 1: + # #1781: no unique type — try a unique top-level FUNCTION definition, + # gated by (a) the stub not being used as a supertype and (b) a + # language-family match with the stub's referrers. + fcands = func_by_label.get(_node_label_key(stub), []) + if len(fcands) == 1 and stub_id not in supertype_stub_ids: + fams = stub_families.get(stub_id, set()) + cand_fam = _lang_family(fcands[0].get("source_file")) + if not fams or cand_fam is None or cand_fam in fams: + candidates = fcands + if len(candidates) != 1: + continue + target_id = candidates[0].get("id") + if isinstance(target_id, str) and target_id and target_id != stub_id: + remap[stub_id] = target_id -def _sv_first_identifier(node, source: bytes) -> str | None: - """First `simple_identifier` under node in pre-order, or None. + if not remap: + return - tree-sitter-verilog 1.0.3 nests declaration names a few levels deep instead - of exposing a `name` field. Scope the search to the right child node (e.g. - `function_identifier`) or this returns the return-type instead of the name. - """ - if node is None: - return None - for child in node.children: - if child.type == "simple_identifier": - return _read_text(child, source) - found = _sv_first_identifier(child, source) - if found: - return found - return None + by_id = {node.get("id"): node for node in nodes if node.get("id")} + csharp_scoped_relations = {"inherits", "implements", "references", "imports"} + for edge in edges: + is_csharp_scoped_edge = ( + str(edge.get("source_file", "")).endswith(".cs") + and edge.get("relation") in csharp_scoped_relations + ) + source = edge.get("source") + if source in remap: + remapped_source = remap[str(source)] + if not ( + is_csharp_scoped_edge + and str(by_id.get(remapped_source, {}).get("source_file", "")).endswith(".cs") + ): + edge["source"] = remapped_source + target = edge.get("target") + if target in remap: + remapped_target = remap[str(target)] + if not ( + is_csharp_scoped_edge + and str(by_id.get(remapped_target, {}).get("source_file", "")).endswith(".cs") + ): + edge["target"] = remapped_target + referenced = {x for e in edges for x in (e.get("source"), e.get("target"))} + drop_ids = {stub_id for stub_id in remap if stub_id not in referenced} + nodes[:] = [node for node in nodes if node.get("id") not in drop_ids] -def _sv_child(node, type_name: str) -> object | None: - if node is None: - return None - for child in node.children: - if child.type == type_name: - return child - return None +def _augment_js_reexport_edges( + paths: list[Path], + nodes: list[dict], + edges: list[dict], + root: Path, +) -> None: + """Compatibility wrapper for the JS/TS symbol-resolution post-pass.""" + facts = _SymbolResolutionFacts() + _collect_js_symbol_resolution_facts(paths, facts) + _apply_symbol_resolution_facts(paths, nodes, edges, root, facts) -_SV_BUILTIN_TYPES = frozenset({ - "bit", "logic", "reg", "wire", "int", "integer", "shortint", "longint", - "byte", "time", "real", "shortreal", "void", "string", "type", "event", - "mailbox", "semaphore", "process", "chandle", -}) -_SV_NON_TYPE_WORDS = frozenset({ - "return", "if", "else", "for", "foreach", "while", "case", "begin", "end", - "function", "task", "class", "endclass", "endfunction", "endtask", -}) - -# One level of balanced parens (e.g. `Foo #(Bar #(int))`) — bounded so malformed -# input cannot trigger pathological backtracking. -_SV_PARENS_INNER = r"(?:[^()]|\([^()]*\))*" -_SV_PARENS = r"\(" + _SV_PARENS_INNER + r"\)" - -_SV_FUNC_RE = re.compile( - r"\bfunction\s+([A-Za-z_]\w*(?:\s*#\s*" + _SV_PARENS + r")?)\s+(\w+)\s*" - r"\((" + _SV_PARENS_INNER + r")\)\s*;", - re.MULTILINE, -) - -_SV_PARAM_RE = re.compile( - r"\s*(?:input|output|inout|ref|const\s+ref)?\s*" - r"([A-Za-z_]\w*(?:\s*#\s*" + _SV_PARENS + r")?)\s+\w+" -) - - -def _sv_strip_comments(text: str) -> str: - text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL) - return re.sub(r"//.*", "", text) - - -def _sv_split_type_list(text: str) -> list[str]: - parts: list[str] = [] - depth = 0 - start = 0 - for idx, ch in enumerate(text): - if ch == "(": - depth += 1 - elif ch == ")": - depth = max(0, depth - 1) - elif ch == "," and depth == 0: - item = text[start:idx].strip() - if item: - parts.append(item) - start = idx + 1 - item = text[start:].strip() - if item: - parts.append(item) - return parts - - -def _sv_collect_type_refs(type_text: str, generic: bool = False, - skip: frozenset[str] = frozenset()) -> list[tuple[str, str]]: - refs: list[tuple[str, str]] = [] - text = type_text.strip() - if not text: - return refs - head = re.match(r"([A-Za-z_]\w*)", text) - if head: - name = head.group(1) - # `skip` carries the enclosing class's `#(type T = ...)` parameters so - # they are not mistaken for referenced types. - if name not in _SV_BUILTIN_TYPES and name not in _SV_NON_TYPE_WORDS and name not in skip: - refs.append((name, "generic_arg" if generic else "type")) - params = re.search(r"#\s*\((" + _SV_PARENS_INNER + r")\)", text) - if params: - for arg in _sv_split_type_list(params.group(1)): - refs.extend(_sv_collect_type_refs(arg, generic=True, skip=skip)) - return refs - - -def _augment_systemverilog_semantics( - raw: str, - stem: str, - str_path: str, - file_nid: str, - nodes: list[dict], - edges: list[dict], - seen_ids: set[str], -) -> None: - label_to_nid = {node["label"]: node["id"] for node in nodes} - - def line_for(offset: int) -> int: - return raw.count("\n", 0, offset) + 1 - - def add_node(nid: str, label: str, line: int) -> None: - if nid not in seen_ids: - seen_ids.add(nid) - nodes.append({"id": nid, "label": label, "file_type": "code", - "source_file": str_path, "source_location": f"L{line}", - "confidence_score": 1.0}) - label_to_nid[label] = nid - - def ensure_type(label: str, line: int) -> str: - if label in label_to_nid: - return label_to_nid[label] - nid = _make_id(stem, label) - add_node(nid, label, line) - return nid - - def add_edge(src: str, target_label: str, relation: str, line: int, context: str | None = None) -> None: - tgt = ensure_type(target_label, line) - edge = {"source": src, "target": tgt, "relation": relation, - "confidence": "EXTRACTED", "confidence_score": 1.0, - "source_file": str_path, "source_location": f"L{line}", "weight": 1.0} - if context: - edge["context"] = context - edges.append(edge) - - text = _sv_strip_comments(raw) - # Consuming `endclass` (rather than a lookahead) makes each match own its - # terminator, so back-to-back or malformed classes cannot bleed bodies. - class_re = re.compile( - r"\b(?:(interface)\s+)?class\s+(\w+)([^;{]*)\s*;(.*?)\bendclass\b", - re.DOTALL, - ) - for match in class_re.finditer(text): - class_name = match.group(2) - header = match.group(3) or "" - body = match.group(4) or "" - line = line_for(match.start()) - # `#(type T = Payload)` declares `T` as a class type parameter, not a - # referenced type — collect these to skip below. - type_params = frozenset(re.findall(r"\btype\s+(\w+)", header)) - class_nid = _make_id(stem, class_name) - add_node(class_nid, class_name, line) - edges.append({"source": file_nid, "target": class_nid, "relation": "defines", - "confidence": "EXTRACTED", "confidence_score": 1.0, - "source_file": str_path, "source_location": f"L{line}", "weight": 1.0}) - - ext = re.search(r"\bextends\s+(\w+)", header) - if ext: - add_edge(class_nid, ext.group(1), "inherits", line) - impl = re.search(r"\bimplements\s+([^;{]+)", header) - if impl: - for iface_name in _sv_split_type_list(impl.group(1)): - add_edge(class_nid, iface_name.split("#", 1)[0].strip(), "implements", line) - - body_without_functions = re.sub( - r"\bfunction\b.*?\bendfunction\b", - lambda m: "\n" * m.group(0).count("\n"), - body, - flags=re.DOTALL, - ) - for field in re.finditer(r"^\s*([A-Za-z_]\w*(?:\s*#\s*\([^;]+?\))?)\s+\w+\s*;", body_without_functions, re.MULTILINE): - # Count to the start of the type token (group 1), not the match - # start: `^\s*` consumes the leading newline(s), so field.start() - # would resolve to the class's line instead of the field's. - field_line = line + body_without_functions.count("\n", 0, field.start(1)) - for ref_name, role in _sv_collect_type_refs(field.group(1), skip=type_params): - add_edge(class_nid, ref_name, "references", field_line, "generic_arg" if role == "generic_arg" else "field") - - for fm in _SV_FUNC_RE.finditer(body): - return_type, func_name, params = fm.group(1), fm.group(2), fm.group(3) - func_line = line + body.count("\n", 0, fm.start()) - func_nid = _make_id(class_nid, func_name) - add_node(func_nid, func_name, func_line) - edges.append({"source": class_nid, "target": func_nid, "relation": "method", - "confidence": "EXTRACTED", "confidence_score": 1.0, - "source_file": str_path, "source_location": f"L{func_line}", "weight": 1.0}) - for ref_name, role in _sv_collect_type_refs(return_type, skip=type_params): - add_edge(func_nid, ref_name, "references", func_line, "generic_arg" if role == "generic_arg" else "return_type") - for param in _sv_split_type_list(params): - pm = _SV_PARAM_RE.match(param) - if not pm: - continue - for ref_name, role in _sv_collect_type_refs(pm.group(1), skip=type_params): - add_edge(func_nid, ref_name, "references", func_line, "generic_arg" if role == "generic_arg" else "parameter_type") - - -def extract_verilog(path: Path) -> dict: - """Extract modules, functions, tasks, package imports, instantiations, and - SystemVerilog class semantics (inherits/implements edges, field/parameter/ - return-type references) from .v/.sv files.""" - try: - import tree_sitter_verilog as tsverilog - from tree_sitter import Language, Parser - except ImportError: - return {"nodes": [], "edges": [], "error": "tree_sitter_verilog not installed"} - - try: - language = Language(tsverilog.language()) - parser = Parser(language) - source = path.read_bytes() - tree = parser.parse(source) - root = tree.root_node - except Exception as e: - return {"nodes": [], "edges": [], "error": str(e)} - - stem = _file_stem(path) - str_path = str(path) - nodes: list[dict] = [] - edges: list[dict] = [] - seen_ids: set[str] = set() - - def add_node(nid: str, label: str, line: int) -> None: - if nid not in seen_ids: - seen_ids.add(nid) - nodes.append({"id": nid, "label": label, "file_type": "code", - "source_file": str_path, "source_location": f"L{line}", - "confidence_score": 1.0}) - - def add_edge(src: str, tgt: str, relation: str, line: int, - confidence: str = "EXTRACTED", score: float = 1.0) -> None: - edges.append({"source": src, "target": tgt, "relation": relation, - "confidence": confidence, "confidence_score": score, - "source_file": str_path, "source_location": f"L{line}", "weight": 1.0}) - - file_nid = _make_id(str(path)) - add_node(file_nid, path.name, 1) - - def walk(node, module_nid: str | None = None) -> None: - t = node.type - - # SystemVerilog class bodies are handled by _augment_systemverilog_semantics - # (regex over source text). Skip their subtrees so in-class methods are not - # double-emitted here — and with the wrong, return-type-derived name. - if t in ("class_declaration", "interface_class_declaration"): - return - - if t == "module_declaration": - mod_name = _sv_first_identifier(_sv_child(node, "module_header"), source) - if mod_name: - line = node.start_point[0] + 1 - nid = _make_id(stem, mod_name) - add_node(nid, mod_name, line) - add_edge(file_nid, nid, "defines", line) - for child in node.children: - walk(child, nid) - return - - # `function_prototype` only appears inside class/interface-class bodies - # (skipped above) and nests its name differently; it is intentionally not - # handled here. - elif t == "function_declaration": - fn_body = _sv_child(node, "function_body_declaration") - func_name = _sv_first_identifier(_sv_child(fn_body, "function_identifier"), source) - if func_name: - line = node.start_point[0] + 1 - parent = module_nid or file_nid - nid = _make_id(parent, func_name) - add_node(nid, f"{func_name}()", line) - add_edge(parent, nid, "contains", line) - - elif t == "task_declaration": - tk_body = _sv_child(node, "task_body_declaration") - task_name = _sv_first_identifier(_sv_child(tk_body, "task_identifier"), source) - if task_name: - line = node.start_point[0] + 1 - parent = module_nid or file_nid - nid = _make_id(parent, task_name) - add_node(nid, task_name, line) - add_edge(parent, nid, "contains", line) - - elif t == "package_import_declaration": - for child in node.children: - if child.type == "package_import_item": - pkg_text = _read_text(child, source) - pkg_name = pkg_text.split("::")[0].strip() - if pkg_name: - line = node.start_point[0] + 1 - tgt_nid = _make_id(pkg_name) - add_node(tgt_nid, pkg_name, line) - src_nid = module_nid or file_nid - add_edge(src_nid, tgt_nid, "imports_from", line) - - elif t in ("module_instantiation", "checker_instantiation"): - # `leaf u_leaf();` parses as checker_instantiation in 1.0.3; - # module_instantiation (when it occurs) exposes a `module_type` field. - # Both reduce to the first identifier under the node — the instantiated - # type, not the instance name (which appears later). - if module_nid: - type_node = node.child_by_field_name("module_type") - inst_type = (_read_text(type_node, source).strip() if type_node - else _sv_first_identifier(node, source)) - if inst_type: - line = node.start_point[0] + 1 - tgt_nid = _make_id(inst_type) - add_node(tgt_nid, inst_type, line) - add_edge(module_nid, tgt_nid, "instantiates", line) - - for child in node.children: - walk(child, module_nid) - - walk(root) - _augment_systemverilog_semantics( - source.decode("utf-8", errors="replace"), - stem, - str_path, - file_nid, - nodes, - edges, - seen_ids, - ) - return {"nodes": nodes, "edges": edges} - - -def extract_sql(path: Path, content: str | bytes | None = None) -> dict: - """Extract tables, views, functions, and relationships from .sql files via tree-sitter.""" - try: - import tree_sitter_sql as tssql - from tree_sitter import Language, Parser - except ImportError: - return {"nodes": [], "edges": [], "error": "tree_sitter_sql not installed. Run: pip install tree-sitter-sql"} - - try: - language = Language(tssql.language()) - parser = Parser(language) - source = ( - content.encode("utf-8") if isinstance(content, str) - else content if content is not None - else path.read_bytes() - ) - tree = parser.parse(source) - root = tree.root_node - except Exception as e: - return {"nodes": [], "edges": [], "error": str(e)} - - - stem = _file_stem(path) - str_path = str(path) - file_nid = _make_id(str_path) - nodes: list[dict] = [{"id": file_nid, "label": path.name, "file_type": "code", - "source_file": str_path, "source_location": None}] - edges: list[dict] = [] - seen_ids: set[str] = {file_nid} - table_nids: dict[str, str] = {} # name → nid for reference resolution - - def _read(n) -> str: - return source[n.start_byte:n.end_byte].decode("utf-8", errors="replace") - - def _obj_name(n) -> str | None: - for c in n.children: - if c.type == "object_reference": - return _read(c) - return None - - def _add_node(nid: str, label: str, line: int) -> None: - if nid not in seen_ids: - seen_ids.add(nid) - nodes.append({"id": nid, "label": label, "file_type": "code", - "source_file": str_path, "source_location": f"L{line}"}) - edges.append({"source": file_nid, "target": nid, "relation": "contains", - "confidence": "EXTRACTED", "source_file": str_path, - "source_location": f"L{line}", "weight": 1.0}) - - def _add_edge(src: str, tgt: str, relation: str, line: int) -> None: - edges.append({"source": src, "target": tgt, "relation": relation, - "confidence": "EXTRACTED", "source_file": str_path, - "source_location": f"L{line}", "weight": 1.0}) - - def walk(node) -> None: - t = node.type - line = node.start_point[0] + 1 - - if t == "create_table": - name = _obj_name(node) - if name: - nid = _make_id(stem, name) - _add_node(nid, name, line) - table_nids[name.lower()] = nid - # Foreign key REFERENCES - for col in node.children: - if col.type == "column_definitions": - has_error = any(cd.type == "ERROR" for cd in col.children) - seen_refs: set[str] = set() - for cd in col.children: - if cd.type == "column_definition": - # Inline column-level REFERENCES - ref_name: str | None = None - found_ref = False - for cc in cd.children: - if cc.type == "keyword_references": - found_ref = True - elif found_ref and cc.type == "object_reference": - ref_name = _read(cc) - break - if ref_name: - ref_nid = table_nids.get(ref_name.lower()) or _make_id(stem, ref_name) - _add_edge(nid, ref_nid, "references", line) - seen_refs.add(ref_name.lower()) - elif cd.type == "constraints": - # Table-level FOREIGN KEY ... REFERENCES ... constraints - for constraint in cd.children: - if constraint.type != "constraint": - continue - ref_name = None - found_ref = False - for cc in constraint.children: - if cc.type == "keyword_references": - found_ref = True - elif found_ref and cc.type == "object_reference": - ref_name = _read(cc) - break - if ref_name: - ref_nid = table_nids.get(ref_name.lower()) or _make_id(stem, ref_name) - _add_edge(nid, ref_nid, "references", line) - seen_refs.add(ref_name.lower()) - if has_error: - # Dialect-specific syntax (e.g. Firebird COMPUTED BY) causes ERROR - # nodes that make the parser drop the trailing constraints block. - # Regex-scan the raw column_definitions text as fallback. - col_text = _read(col) - for rm in re.finditer(r"\bREFERENCES\s+([\w$]+)", col_text, re.IGNORECASE): - ref_name = rm.group(1) - if ref_name.lower() not in seen_refs: - ref_nid = table_nids.get(ref_name.lower()) or _make_id(stem, ref_name) - _add_edge(nid, ref_nid, "references", line) - seen_refs.add(ref_name.lower()) - - elif t == "create_view": - name = _obj_name(node) - if name: - nid = _make_id(stem, name) - _add_node(nid, name, line) - table_nids[name.lower()] = nid - # FROM/JOIN table references inside view body - _walk_from_refs(node, nid, line) - - elif t == "create_function": - name = _obj_name(node) - if name: - nid = _make_id(stem, name) - _add_node(nid, f"{name}()", line) - _walk_from_refs(node, nid, line) - - elif t == "create_procedure": - name = _obj_name(node) - if name: - nid = _make_id(stem, name) - _add_node(nid, f"{name}()", line) - _walk_from_refs(node, nid, line) - - elif t == "alter_table": - name = _obj_name(node) - if name: - src_nid = table_nids.get(name.lower()) - if not src_nid: - src_nid = _make_id(stem, name) - _add_node(src_nid, name, line) - table_nids[name.lower()] = src_nid - for child in node.children: - if child.type == "add_constraint": - for cc in child.children: - if cc.type != "constraint": - continue - found_ref = False - ref_name: str | None = None - for ccc in cc.children: - if ccc.type == "keyword_references": - found_ref = True - elif found_ref and ccc.type == "object_reference": - ref_name = _read(ccc) - break - if ref_name: - ref_nid = table_nids.get(ref_name.lower()) - if not ref_nid: - ref_nid = _make_id(stem, ref_name) - _add_edge(src_nid, ref_nid, "references", line) - - elif t == "create_trigger": - trig_name: str | None = None - tbl_name: str | None = None - after_trigger = False - after_for = False - for c in node.children: - if c.type == "keyword_trigger": - after_trigger = True - elif after_trigger and not trig_name and c.type == "object_reference": - trig_name = _read(c) - elif c.type == "keyword_for": - after_for = True - elif after_for and not tbl_name and c.type == "object_reference": - tbl_name = _read(c) - if trig_name: - trig_nid = _make_id(stem, trig_name) - _add_node(trig_nid, trig_name, line) - if tbl_name: - tbl_nid = table_nids.get(tbl_name.lower()) or _make_id(stem, tbl_name) - _add_edge(trig_nid, tbl_nid, "triggers", line) - - elif t == "fb_proc_or_trigger": - text = _read(node) - m = re.match( - r"CREATE\s+(?:OR\s+(?:REPLACE|ALTER)\s+)?" - r"(PROCEDURE|TRIGGER|FUNCTION)\s+([\w$]+)", - text, re.IGNORECASE, - ) - if m: - obj_type = m.group(1).upper() - obj_name = m.group(2) - obj_nid = _make_id(stem, obj_name) - label = obj_name if obj_type == "TRIGGER" else f"{obj_name}()" - _add_node(obj_nid, label, line) - if obj_type == "TRIGGER": - fm = re.search(r"\bFOR\s+([\w$]+)", text, re.IGNORECASE) - if fm: - tbl = fm.group(1) - tbl_nid = table_nids.get(tbl.lower()) or _make_id(stem, tbl) - _add_edge(obj_nid, tbl_nid, "triggers", line) - _NON_TABLES = { - "select", "where", "set", "dual", "null", "true", "false", - "first", "skip", "rows", "next", "only", "lateral", - } - seen_tbls: set[str] = set() - for rm in re.finditer(r"\b(?:FROM|JOIN|INTO)\s+([\w$]+)", text, re.IGNORECASE): - tbl = rm.group(1) - if tbl.lower() not in _NON_TABLES and tbl.lower() not in seen_tbls: - seen_tbls.add(tbl.lower()) - tbl_nid = table_nids.get(tbl.lower()) or _make_id(stem, tbl) - _add_edge(obj_nid, tbl_nid, "reads_from", line) - for rm in re.finditer(r"\bUPDATE\s+([\w$]+)", text, re.IGNORECASE): - tbl = rm.group(1) - if tbl.lower() not in _NON_TABLES and tbl.lower() not in seen_tbls: - seen_tbls.add(tbl.lower()) - tbl_nid = table_nids.get(tbl.lower()) or _make_id(stem, tbl) - _add_edge(obj_nid, tbl_nid, "reads_from", line) - - for child in node.children: - walk(child) - - def _walk_from_refs(node, caller_nid: str, line: int) -> None: - """Recursively find FROM/JOIN table references inside a node.""" - if node.type in ("from", "join"): - for c in node.children: - if c.type == "relation": - for cc in c.children: - if cc.type == "object_reference": - tbl = _read(cc) - tbl_nid = _make_id(stem, tbl) - _add_edge(caller_nid, tbl_nid, "reads_from", - c.start_point[0] + 1) - for child in node.children: - _walk_from_refs(child, caller_nid, line) - - for stmt in root.children: - if stmt.type == "statement": - for child in stmt.children: - walk(child) - elif stmt.type in ("fb_proc_or_trigger", "set_term", "declare_external_function"): - walk(stmt) - - # Global regex fallback: catch any REFERENCES missed due to ERROR nodes in the parse tree - # (e.g. Firebird COMPUTED BY columns push constraints out of the tree entirely). - # Snapshot after tree walk so we don't re-emit edges already captured above. - emitted = {(e["source"], e["target"]) for e in edges if e["relation"] == "references"} - src_text = source.decode("utf-8", errors="replace") - for m in re.finditer(r"CREATE\s+TABLE\s+([\w$]+)\s*\(", src_text, re.IGNORECASE): - tbl_name = m.group(1) - tbl_nid = table_nids.get(tbl_name.lower()) - if tbl_nid is None: - continue - tbl_line = src_text[: m.start()].count("\n") + 1 - tail = src_text[m.start():] - end = re.search(r"(?:^|\n)(?:CREATE|SET\s+TERM|ALTER)\s", tail[1:], re.IGNORECASE) - block = tail[: end.start() + 1] if end else tail - for rm in re.finditer(r"\bREFERENCES\s+([\w$]+)", block, re.IGNORECASE): - ref_name = rm.group(1) - ref_nid = table_nids.get(ref_name.lower()) or _make_id(stem, ref_name) - if (tbl_nid, ref_nid) not in emitted: - _add_edge(tbl_nid, ref_nid, "references", tbl_line) - emitted.add((tbl_nid, ref_nid)) - - return {"nodes": nodes, "edges": edges} - - -def extract_lua(path: Path) -> dict: - """Extract functions, methods, require() imports, and calls from a .lua file.""" - return _extract_generic(path, _LUA_CONFIG) - - -def extract_swift(path: Path) -> dict: - """Extract classes, structs, protocols, functions, imports, and calls from a .swift file.""" - return _extract_generic(path, _SWIFT_CONFIG) - - -# ── Julia extractor (custom walk) ──────────────────────────────────────────── - -def extract_julia(path: Path) -> dict: - """Extract modules, structs, functions, imports, and calls from a .jl file.""" - try: - import tree_sitter_julia as tsjulia - from tree_sitter import Language, Parser - except ImportError: - return {"nodes": [], "edges": [], "error": "tree-sitter-julia not installed"} - - try: - language = Language(tsjulia.language()) - parser = Parser(language) - source = path.read_bytes() - tree = parser.parse(source) - root = tree.root_node - except Exception as e: - return {"nodes": [], "edges": [], "error": str(e)} - - stem = _file_stem(path) - str_path = str(path) - nodes: list[dict] = [] - edges: list[dict] = [] - seen_ids: set[str] = set() - function_bodies: list[tuple[str, object]] = [] - - def add_node(nid: str, label: str, line: int) -> None: - if nid not in seen_ids: - seen_ids.add(nid) - nodes.append({ - "id": nid, - "label": label, - "file_type": "code", - "source_file": str_path, - "source_location": f"L{line}", - }) - - def add_edge(src: str, tgt: str, relation: str, line: int, - confidence: str = "EXTRACTED", weight: float = 1.0, - context: str | None = None) -> None: - edge = { - "source": src, - "target": tgt, - "relation": relation, - "confidence": confidence, - "source_file": str_path, - "source_location": f"L{line}", - "weight": weight, - } - if context: - edge["context"] = context - edges.append(edge) - - file_nid = _make_id(str(path)) - add_node(file_nid, path.name, 1) - - def ensure_named_node(name: str, line: int) -> str: - nid = _make_id(stem, name) - if nid in seen_ids: - return nid - nid = _make_id(name) - if nid not in seen_ids: - # The name isn't defined in this file, so this is a cross-file reference - # (e.g. a `Thing` type annotation imported from another module). Emit a - # SOURCELESS stub — like the inheritance-base path below — so the - # corpus-level rewire can collapse it onto the real definition. A sourced - # stub here makes _disambiguate_colliding_node_ids bake the referencing - # file's path (with extension) into the id and blocks the rewire, which is - # the phantom-duplicate-node bug (#1402). - seen_ids.add(nid) - nodes.append({ - "id": nid, - "label": name, - "file_type": "code", - "source_file": "", - "source_location": "", - "origin_file": str_path, - }) - return nid - - def _func_name_from_signature(sig_node) -> str | None: - """Extract function name from a Julia signature node (call_expression > identifier).""" - for child in sig_node.children: - if child.type == "call_expression": - callee = child.children[0] if child.children else None - if callee and callee.type == "identifier": - return _read_text(callee, source) - return None - - def walk_calls(body_node, func_nid: str) -> None: - if body_node is None: - return - t = body_node.type - if t in ("function_definition", "short_function_definition"): - return - if t == "call_expression" and body_node.children: - callee = body_node.children[0] - # Direct call: foo(...) - if callee.type == "identifier": - callee_name = _read_text(callee, source) - target_nid = _make_id(stem, callee_name) - add_edge(func_nid, target_nid, "calls", body_node.start_point[0] + 1, - confidence="EXTRACTED", context="call") - # Method call: obj.method(...) - elif callee.type == "field_expression" and len(callee.children) >= 3: - method_node = callee.children[-1] - method_name = _read_text(method_node, source) - target_nid = _make_id(stem, method_name) - add_edge(func_nid, target_nid, "calls", body_node.start_point[0] + 1, - confidence="EXTRACTED", context="call") - for child in body_node.children: - walk_calls(child, func_nid) - - def walk(node, scope_nid: str) -> None: - t = node.type - - # Module - if t == "module_definition": - name_node = next((c for c in node.children if c.type == "identifier"), None) - if name_node: - mod_name = _read_text(name_node, source) - mod_nid = _make_id(stem, mod_name) - line = node.start_point[0] + 1 - add_node(mod_nid, mod_name, line) - add_edge(file_nid, mod_nid, "defines", line) - for child in node.children: - walk(child, mod_nid) - return - - # Struct (struct / mutable struct — both map to struct_definition in tree-sitter-julia) - if t == "struct_definition": - # type_head may contain: identifier (simple) or binary_expression (Foo <: Bar) - type_head = next((c for c in node.children if c.type == "type_head"), None) - if not type_head: - return - struct_name: str | None = None - super_name: str | None = None - bin_expr = next((c for c in type_head.children if c.type == "binary_expression"), None) - if bin_expr: - identifiers = [c for c in bin_expr.children if c.type == "identifier"] - if identifiers: - struct_name = _read_text(identifiers[0], source) - if len(identifiers) >= 2: - super_name = _read_text(identifiers[-1], source) - else: - name_node = next((c for c in type_head.children if c.type == "identifier"), None) - if name_node: - struct_name = _read_text(name_node, source) - if not struct_name: - return - struct_nid = _make_id(stem, struct_name) - line = node.start_point[0] + 1 - add_node(struct_nid, struct_name, line) - add_edge(scope_nid, struct_nid, "defines", line) - if super_name: - add_edge(struct_nid, ensure_named_node(super_name, line), - "inherits", line, confidence="EXTRACTED") - # Field types: each `name::Type` lowers to a typed_expression child of struct_definition - for child in node.children: - if child.type == "typed_expression": - type_ids = [c for c in child.children if c.type == "identifier"] - if len(type_ids) >= 2: - field_line = child.start_point[0] + 1 - type_name = _read_text(type_ids[-1], source) - type_nid = ensure_named_node(type_name, field_line) - edges.append(_semantic_reference_edge( - struct_nid, type_nid, "field", str_path, field_line)) - return - - # Abstract type - if t == "abstract_definition": - # type_head > identifier - type_head = next((c for c in node.children if c.type == "type_head"), None) - if type_head: - name_node = next((c for c in type_head.children if c.type == "identifier"), None) - if name_node: - abs_name = _read_text(name_node, source) - abs_nid = _make_id(stem, abs_name) - line = node.start_point[0] + 1 - add_node(abs_nid, abs_name, line) - add_edge(scope_nid, abs_nid, "defines", line) - return - - # Function: function foo(...) ... end - if t == "function_definition": - sig_node = next((c for c in node.children if c.type == "signature"), None) - if sig_node: - func_name = _func_name_from_signature(sig_node) - if func_name: - func_nid = _make_id(stem, func_name) - line = node.start_point[0] + 1 - add_node(func_nid, f"{func_name}()", line) - add_edge(scope_nid, func_nid, "defines", line) - function_bodies.append((func_nid, node)) - return - - # Short function: foo(x) = expr - if t == "assignment": - lhs = node.children[0] if node.children else None - if lhs and lhs.type == "call_expression" and lhs.children: - callee = lhs.children[0] - if callee.type == "identifier": - func_name = _read_text(callee, source) - func_nid = _make_id(stem, func_name) - line = node.start_point[0] + 1 - add_node(func_nid, f"{func_name}()", line) - add_edge(scope_nid, func_nid, "defines", line) - # Only walk the RHS (index 2 after lhs and operator) to avoid self-loops - rhs = node.children[-1] if len(node.children) >= 3 else None - if rhs: - function_bodies.append((func_nid, rhs)) - return - - # Using / Import - if t in ("using_statement", "import_statement"): - line = node.start_point[0] + 1 - for child in node.children: - if child.type == "identifier": - mod_name = _read_text(child, source) - imp_nid = _make_id(mod_name) - add_node(imp_nid, mod_name, line) - add_edge(scope_nid, imp_nid, "imports", line, context="import") - elif child.type == "selected_import": - identifiers = [c for c in child.children if c.type == "identifier"] - if identifiers: - pkg_name = _read_text(identifiers[0], source) - pkg_nid = _make_id(pkg_name) - add_node(pkg_nid, pkg_name, line) - add_edge(scope_nid, pkg_nid, "imports", line, context="import") - return - - for child in node.children: - walk(child, scope_nid) - - walk(root, file_nid) - - for func_nid, body_node in function_bodies: - # For function_definition nodes, walk children directly to avoid - # the boundary check returning early on the top-level node itself. - # Skip the "signature" child — it contains the function's own call_expression - # which would create a self-loop. - if body_node.type == "function_definition": - for child in body_node.children: - if child.type != "signature": - walk_calls(child, func_nid) - else: - walk_calls(body_node, func_nid) - - return {"nodes": nodes, "edges": edges} - - -_FORTRAN_CPP_EXTS = {".F", ".F90", ".F95", ".F03", ".F08"} - - -def _cpp_preprocess(path: Path) -> bytes: - """Run cpp -w -P on a capital-F Fortran file and return preprocessed bytes. - - Falls back to raw file bytes if cpp is not available. Capital-F extensions - conventionally require C preprocessor expansion (#ifdef MPI, #define REAL8, etc.) - before parsing. - - Security (F-007): we pass `-nostdinc` and `-I /dev/null` so a malicious - source file containing `#include "/home/victim/.ssh/id_rsa"` (or any other - include directive) cannot inline arbitrary host files into the output that - we then ship to an LLM. Without these flags `cpp` happily resolves any - relative or absolute include path it can read, which is a corpus-side - file-exfiltration vector. - """ - import shutil - import subprocess - if not shutil.which("cpp"): - return path.read_bytes() - try: - # Pass an absolute path so a corpus file named like "-I/etc/x.F90" cannot - # be parsed by cpp as an option (cpp does not accept a "--" end-of-options - # terminator). An absolute path always begins with "/". - result = subprocess.run( - ["cpp", "-w", "-P", "-nostdinc", "-I", "/dev/null", str(path.resolve())], - capture_output=True, - timeout=30, - ) - if result.returncode == 0 and result.stdout: - return result.stdout - except Exception: - pass - return path.read_bytes() - - -def extract_fortran(path: Path) -> dict: - """Extract programs, modules, subroutines, functions, use statements, and calls from Fortran files. - - Capital-F extensions (.F, .F90, etc.) are run through the C preprocessor before - parsing so #ifdef/#define macros are resolved. - """ - try: - import tree_sitter_fortran as tsfortran - from tree_sitter import Language, Parser - except ImportError: - return {"nodes": [], "edges": [], "error": "tree-sitter-fortran not installed"} - - try: - language = Language(tsfortran.language()) - parser = Parser(language) - source = _cpp_preprocess(path) if path.suffix in _FORTRAN_CPP_EXTS else path.read_bytes() - tree = parser.parse(source) - root = tree.root_node - except Exception as e: - return {"nodes": [], "edges": [], "error": str(e)} - - stem = _file_stem(path) - str_path = str(path) - nodes: list[dict] = [] - edges: list[dict] = [] - seen_ids: set[str] = set() - scope_bodies: list[tuple[str, object]] = [] - - def add_node(nid: str, label: str, line: int) -> None: - if nid not in seen_ids: - seen_ids.add(nid) - nodes.append({ - "id": nid, - "label": label, - "file_type": "code", - "source_file": str_path, - "source_location": f"L{line}", - }) - - def add_edge(src: str, tgt: str, relation: str, line: int, - confidence: str = "EXTRACTED", weight: float = 1.0, - context: str | None = None) -> None: - edge = { - "source": src, - "target": tgt, - "relation": relation, - "confidence": confidence, - "source_file": str_path, - "source_location": f"L{line}", - "weight": weight, - } - if context: - edge["context"] = context - edges.append(edge) - - file_nid = _make_id(str(path)) - add_node(file_nid, path.name, 1) - - def _fortran_name(stmt_node) -> str | None: - """Extract name from a *_statement node. Fortran is case-insensitive; lowercase.""" - for child in stmt_node.children: - if child.type in ("name", "identifier"): - return _read_text(child, source).lower() - return None - - def ensure_named_node(name: str, line: int) -> str: - nid = _make_id(stem, name) - if nid in seen_ids: - return nid - nid = _make_id(name) - if nid not in seen_ids: - # The name isn't defined in this file, so this is a cross-file reference - # (e.g. a `Thing` type annotation imported from another module). Emit a - # SOURCELESS stub — like the inheritance-base path below — so the - # corpus-level rewire can collapse it onto the real definition. A sourced - # stub here makes _disambiguate_colliding_node_ids bake the referencing - # file's path (with extension) into the id and blocks the rewire, which is - # the phantom-duplicate-node bug (#1402). - seen_ids.add(nid) - nodes.append({ - "id": nid, - "label": name, - "file_type": "code", - "source_file": "", - "source_location": "", - "origin_file": str_path, - }) - return nid - - def emit_signature_refs(scope_node, fn_nid: str, is_function: bool) -> None: - """Emit references[parameter_type] / references[return_type] edges for - a subroutine/function based on its variable_declaration siblings.""" - stmt_type = "function_statement" if is_function else "subroutine_statement" - stmt = next((c for c in scope_node.children if c.type == stmt_type), None) - if stmt is None: - return - param_names: set[str] = set() - params_node = next((c for c in stmt.children if c.type == "parameters"), None) - if params_node is not None: - for c in params_node.children: - if c.type == "identifier": - param_names.add(_read_text(c, source).lower()) - result_name: str | None = None - if is_function: - result_node = next((c for c in stmt.children if c.type == "function_result"), None) - if result_node is not None: - res_id = next((c for c in result_node.children if c.type == "identifier"), None) - if res_id is not None: - result_name = _read_text(res_id, source).lower() - else: - # implicit result variable: same name as the function - result_name = _fortran_name(stmt) - for child in scope_node.children: - if child.type != "variable_declaration": - continue - derived = next((c for c in child.children if c.type == "derived_type"), None) - if derived is None: - continue - type_name_node = next((c for c in derived.children if c.type == "type_name"), None) - if type_name_node is None: - continue - type_name = _read_text(type_name_node, source).lower() - for var in child.children: - if var.type != "identifier": - continue - var_name = _read_text(var, source).lower() - var_line = var.start_point[0] + 1 - if var_name in param_names: - tgt = ensure_named_node(type_name, var_line) - if tgt != fn_nid: - add_edge(fn_nid, tgt, "references", var_line, context="parameter_type") - elif is_function and var_name == result_name: - tgt = ensure_named_node(type_name, var_line) - if tgt != fn_nid: - add_edge(fn_nid, tgt, "references", var_line, context="return_type") - - def walk_calls(node, scope_nid: str) -> None: - if node is None: - return - t = node.type - if t in ("subroutine", "function", "module", "program", "internal_procedures"): - return - # call FOO(args) — tree-sitter-fortran uses subroutine_call - if t == "subroutine_call": - name_node = next((c for c in node.children if c.type == "identifier"), None) - if name_node: - callee = _read_text(name_node, source).lower() - target_nid = _make_id(stem, callee) - add_edge(scope_nid, target_nid, "calls", node.start_point[0] + 1, - confidence="EXTRACTED", context="call") - for child in node.children: - walk_calls(child, scope_nid) - - def walk(node, scope_nid: str) -> None: - t = node.type - - if t == "program": - stmt = next((c for c in node.children if c.type == "program_statement"), None) - name = _fortran_name(stmt) if stmt else None - if name: - nid = _make_id(stem, name) - line = node.start_point[0] + 1 - add_node(nid, name, line) - add_edge(file_nid, nid, "defines", line) - scope_bodies.append((nid, node)) - for child in node.children: - walk(child, nid) - return - - if t == "module": - stmt = next((c for c in node.children if c.type == "module_statement"), None) - name = _fortran_name(stmt) if stmt else None - if name: - nid = _make_id(stem, name) - line = node.start_point[0] + 1 - add_node(nid, name, line) - add_edge(file_nid, nid, "defines", line) - for child in node.children: - walk(child, nid) - return - - # subroutines/functions inside a module live under internal_procedures - if t == "internal_procedures": - for child in node.children: - walk(child, scope_nid) - return - - if t == "derived_type_definition": - stmt = next((c for c in node.children if c.type == "derived_type_statement"), None) - if stmt is not None: - name_node = next((c for c in stmt.children if c.type == "type_name"), None) - if name_node is not None: - type_name = _read_text(name_node, source).lower() - type_nid = _make_id(stem, type_name) - line = node.start_point[0] + 1 - add_node(type_nid, type_name, line) - add_edge(scope_nid, type_nid, "defines", line) - return - - if t == "subroutine": - stmt = next((c for c in node.children if c.type == "subroutine_statement"), None) - name = _fortran_name(stmt) if stmt else None - if name: - nid = _make_id(stem, name) - line = node.start_point[0] + 1 - add_node(nid, f"{name}()", line) - add_edge(scope_nid, nid, "defines", line) - scope_bodies.append((nid, node)) - emit_signature_refs(node, nid, is_function=False) - for child in node.children: - walk(child, nid) - return - - if t == "function": - stmt = next((c for c in node.children if c.type == "function_statement"), None) - name = _fortran_name(stmt) if stmt else None - if name: - nid = _make_id(stem, name) - line = node.start_point[0] + 1 - add_node(nid, f"{name}()", line) - add_edge(scope_nid, nid, "defines", line) - scope_bodies.append((nid, node)) - emit_signature_refs(node, nid, is_function=True) - for child in node.children: - walk(child, nid) - return - - if t == "use_statement": - line = node.start_point[0] + 1 - # tree-sitter-fortran uses module_name node for the used module - name_node = next((c for c in node.children if c.type in ("module_name", "name", "identifier")), None) - if name_node: - mod_name = _read_text(name_node, source).lower() - imp_nid = _make_id(mod_name) - add_node(imp_nid, mod_name, line) - add_edge(scope_nid, imp_nid, "imports", line, context="use") - return - - for child in node.children: - walk(child, scope_nid) - - walk(root, file_nid) - - _stmt_headers = { - "subroutine_statement", "function_statement", - "program_statement", "module_statement", - } - for scope_nid, body_node in scope_bodies: - for child in body_node.children: - if child.type not in _stmt_headers: - walk_calls(child, scope_nid) - - return {"nodes": nodes, "edges": edges} - - -# ── Go extractor (custom walk) ──────────────────────────────────────────────── - -def extract_go(path: Path) -> dict: - """Extract functions, methods, type declarations, and imports from a .go file.""" - try: - import tree_sitter_go as tsgo - from tree_sitter import Language, Parser - except ImportError: - return {"nodes": [], "edges": [], "error": "tree-sitter-go not installed"} - - try: - language = Language(tsgo.language()) - parser = Parser(language) - source = path.read_bytes() - tree = parser.parse(source) - root = tree.root_node - except Exception as e: - return {"nodes": [], "edges": [], "error": str(e)} - - stem = _file_stem(path) - # Use directory name as package scope so methods on the same type across - # multiple files in a package share one canonical type node. - pkg_scope = path.parent.name or stem - str_path = str(path) - nodes: list[dict] = [] - edges: list[dict] = [] - seen_ids: set[str] = set() - function_bodies: list[tuple[str, object]] = [] - go_imported_pkgs: set[str] = set() # local names of imported packages - - def add_node(nid: str, label: str, line: int) -> None: - if nid not in seen_ids: - seen_ids.add(nid) - nodes.append({ - "id": nid, - "label": label, - "file_type": "code", - "source_file": str_path, - "source_location": f"L{line}", - }) - - def add_edge(src: str, tgt: str, relation: str, line: int, - confidence: str = "EXTRACTED", weight: float = 1.0, - context: str | None = None) -> None: - edge = { - "source": src, - "target": tgt, - "relation": relation, - "confidence": confidence, - "source_file": str_path, - "source_location": f"L{line}", - "weight": weight, - } - if context: - edge["context"] = context - edges.append(edge) - - file_nid = _make_id(str(path)) - add_node(file_nid, path.name, 1) - - def ensure_named_node(name: str, line: int) -> str: - nid = _make_id(pkg_scope, name) - if nid in seen_ids: - return nid - nid = _make_id(name) - if nid not in seen_ids: - # The name isn't declared in this file, so this is a cross-file reference - # (e.g. a type defined in another file of the package). Emit a SOURCELESS - # stub — like the inheritance-base path in the other extractors — so the - # corpus-level rewire can collapse it onto the real definition. A sourced - # stub here makes _disambiguate_colliding_node_ids bake the referencing - # file's path (with extension) into the id and blocks the rewire, which is - # the phantom-duplicate-node bug (#1402). - seen_ids.add(nid) - nodes.append({ - "id": nid, - "label": name, - "file_type": "code", - "source_file": "", - "source_location": "", - "origin_file": str_path, - }) - return nid - - def emit_go_method_refs(func_node, func_nid: str, line: int) -> None: - params = func_node.child_by_field_name("parameters") - if params is not None: - for p in params.children: - if p.type != "parameter_declaration": - continue - type_node = p.child_by_field_name("type") - refs: list[tuple[str, str]] = [] - _go_collect_type_refs(type_node, source, False, refs) - for ref_name, role in refs: - ctx = "generic_arg" if role == "generic_arg" else "parameter_type" - tgt = ensure_named_node(ref_name, line) - if tgt != func_nid: - add_edge(func_nid, tgt, "references", line, context=ctx) - result = func_node.child_by_field_name("result") - if result is not None: - if result.type == "parameter_list": - for p in result.children: - if p.type != "parameter_declaration": - continue - type_node = p.child_by_field_name("type") - if type_node is None: - for c in p.children: - if c.is_named: - type_node = c - break - refs = [] - _go_collect_type_refs(type_node, source, False, refs) - for ref_name, role in refs: - ctx = "generic_arg" if role == "generic_arg" else "return_type" - tgt = ensure_named_node(ref_name, line) - if tgt != func_nid: - add_edge(func_nid, tgt, "references", line, context=ctx) - else: - refs = [] - _go_collect_type_refs(result, source, False, refs) - for ref_name, role in refs: - ctx = "generic_arg" if role == "generic_arg" else "return_type" - tgt = ensure_named_node(ref_name, line) - if tgt != func_nid: - add_edge(func_nid, tgt, "references", line, context=ctx) - - def walk(node) -> None: - t = node.type - - if t == "function_declaration": - name_node = node.child_by_field_name("name") - if name_node: - func_name = _read_text(name_node, source) - line = node.start_point[0] + 1 - func_nid = _make_id(stem, func_name) - add_node(func_nid, f"{func_name}()", line) - add_edge(file_nid, func_nid, "contains", line) - emit_go_method_refs(node, func_nid, line) - body = node.child_by_field_name("body") - if body: - function_bodies.append((func_nid, body)) - return - - if t == "method_declaration": - receiver = node.child_by_field_name("receiver") - receiver_type: str | None = None - if receiver: - for param in receiver.children: - if param.type == "parameter_declaration": - type_node = param.child_by_field_name("type") - if type_node: - receiver_type = _read_text(type_node, source).lstrip("*").strip() - break - name_node = node.child_by_field_name("name") - if not name_node: - return - method_name = _read_text(name_node, source) - line = node.start_point[0] + 1 - - if receiver_type: - parent_nid = _make_id(pkg_scope, receiver_type) - add_node(parent_nid, receiver_type, line) - method_nid = _make_id(parent_nid, method_name) - add_node(method_nid, f".{method_name}()", line) - add_edge(parent_nid, method_nid, "method", line) - else: - method_nid = _make_id(stem, method_name) - add_node(method_nid, f"{method_name}()", line) - add_edge(file_nid, method_nid, "contains", line) - - emit_go_method_refs(node, method_nid, line) - body = node.child_by_field_name("body") - if body: - function_bodies.append((method_nid, body)) - return - - if t == "type_declaration": - for child in node.children: - if child.type != "type_spec": - continue - name_node = child.child_by_field_name("name") - if not name_node: - continue - type_name = _read_text(name_node, source) - line = child.start_point[0] + 1 - type_nid = _make_id(pkg_scope, type_name) - add_node(type_nid, type_name, line) - add_edge(file_nid, type_nid, "contains", line) - # Type body: struct fields (with embeds) or interface embedding. - type_body = None - for tc in child.children: - if tc.type in ("struct_type", "interface_type"): - type_body = tc - break - if type_body is None: - continue - if type_body.type == "struct_type": - for fdl in type_body.children: - if fdl.type != "field_declaration_list": - continue - for field in fdl.children: - if field.type != "field_declaration": - continue - has_name = any( - fc.type == "field_identifier" for fc in field.children - ) - type_node = field.child_by_field_name("type") - if type_node is None: - for fc in field.children: - if fc.is_named and fc.type != "field_identifier": - type_node = fc - break - refs: list[tuple[str, str]] = [] - _go_collect_type_refs(type_node, source, False, refs) - for ref_name, role in refs: - tgt = ensure_named_node(ref_name, field.start_point[0] + 1) - if tgt == type_nid: - continue - if not has_name and role == "type": - add_edge(type_nid, tgt, "embeds", - field.start_point[0] + 1) - else: - ctx = "generic_arg" if role == "generic_arg" else "field" - add_edge(type_nid, tgt, "references", - field.start_point[0] + 1, context=ctx) - elif type_body.type == "interface_type": - for elem in type_body.children: - if elem.type != "type_elem": - continue - refs = [] - for sub in elem.children: - if sub.is_named: - _go_collect_type_refs(sub, source, False, refs) - for ref_name, role in refs: - tgt = ensure_named_node(ref_name, elem.start_point[0] + 1) - if tgt == type_nid: - continue - if role == "type": - add_edge(type_nid, tgt, "embeds", - elem.start_point[0] + 1) - else: - add_edge(type_nid, tgt, "references", - elem.start_point[0] + 1, context="generic_arg") - return - - if t == "import_declaration": - for child in node.children: - if child.type == "import_spec_list": - for spec in child.children: - if spec.type == "import_spec": - path_node = spec.child_by_field_name("path") - if path_node: - raw = _read_text(path_node, source).strip('"') - # Prefix with go_pkg_ so stdlib names (e.g. "context") - # don't collide with local files of the same basename. - tgt_nid = _make_id("go", "pkg", raw) - add_edge(file_nid, tgt_nid, "imports_from", spec.start_point[0] + 1, context="import") - # Track local name (alias or last path segment) - alias = spec.child_by_field_name("name") - local_name = _read_text(alias, source) if alias else raw.split("/")[-1] - if local_name and local_name != "_" and local_name != ".": - go_imported_pkgs.add(local_name) - elif child.type == "import_spec": - path_node = child.child_by_field_name("path") - if path_node: - raw = _read_text(path_node, source).strip('"') - tgt_nid = _make_id("go", "pkg", raw) - add_edge(file_nid, tgt_nid, "imports_from", child.start_point[0] + 1, context="import") - alias = child.child_by_field_name("name") - local_name = _read_text(alias, source) if alias else raw.split("/")[-1] - if local_name and local_name != "_" and local_name != ".": - go_imported_pkgs.add(local_name) - return - - for child in node.children: - walk(child) - - walk(root) - - label_to_nid: dict[str, str] = {} - for n in nodes: - raw = n["label"] - normalised = raw.strip("()").lstrip(".") - label_to_nid[normalised] = n["id"] - - seen_call_pairs: set[tuple[str, str]] = set() - raw_calls: list[dict] = [] - - def walk_calls(node, caller_nid: str) -> None: - if node.type in ("function_declaration", "method_declaration"): - return - if node.type == "call_expression": - func_node = node.child_by_field_name("function") - callee_name: str | None = None - is_member_call: bool = False - if func_node: - if func_node.type == "identifier": - callee_name = _read_text(func_node, source) - elif func_node.type == "selector_expression": - field = func_node.child_by_field_name("field") - operand = func_node.child_by_field_name("operand") - receiver_name = _read_text(operand, source) if operand else "" - # Package-qualified call (e.g. fmt.Println) → allow cross-file resolution. - # Receiver method call (e.g. s.logger.Log) → skip, no import evidence. - is_member_call = receiver_name not in go_imported_pkgs - if field: - callee_name = _read_text(field, source) - if callee_name and callee_name not in _LANGUAGE_BUILTIN_GLOBALS: - tgt_nid = label_to_nid.get(callee_name) - if tgt_nid and tgt_nid != caller_nid: - pair = (caller_nid, tgt_nid) - if pair not in seen_call_pairs: - seen_call_pairs.add(pair) - line = node.start_point[0] + 1 - edges.append({ - "source": caller_nid, - "target": tgt_nid, - "relation": "calls", - "context": "call", - "confidence": "EXTRACTED", - "source_file": str_path, - "source_location": f"L{line}", - "weight": 1.0, - }) - elif callee_name: - raw_calls.append({ - "caller_nid": caller_nid, - "callee": callee_name, - "is_member_call": is_member_call, - "source_file": str_path, - "source_location": f"L{node.start_point[0] + 1}", - }) - for child in node.children: - walk_calls(child, caller_nid) - - for caller_nid, body_node in function_bodies: - walk_calls(body_node, caller_nid) - - valid_ids = seen_ids - clean_edges = [] - for edge in edges: - src, tgt = edge["source"], edge["target"] - if src in valid_ids and (tgt in valid_ids or edge["relation"] in ("imports", "imports_from")): - clean_edges.append(edge) - - return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls} - - -# ── Rust extractor (custom walk) ────────────────────────────────────────────── - -# Common Rust trait/stdlib method names that appear in virtually every codebase. -# Resolving these cross-file produces spurious INFERRED edges across crate -# boundaries (issue #908) — skip them from the unresolved-call queue entirely. -_RUST_TRAIT_METHOD_BLOCKLIST: frozenset[str] = frozenset({ - "new", "default", "parse", "from_str", "now", "clone", "into", "from", - "to_string", "to_owned", "len", "is_empty", "iter", "next", "build", - "start", "run", "init", "app", "get", "set", "push", "pop", "insert", - "remove", "contains", "collect", "map", "filter", "unwrap", "expect", - "ok", "err", "some", "none", "send", "recv", "lock", "read", "write", -}) - -def extract_rust(path: Path) -> dict: - """Extract functions, structs, enums, traits, impl methods, and use declarations from a .rs file.""" - try: - import tree_sitter_rust as tsrust - from tree_sitter import Language, Parser - except ImportError: - return {"nodes": [], "edges": [], "error": "tree-sitter-rust not installed"} - - try: - language = Language(tsrust.language()) - parser = Parser(language) - source = path.read_bytes() - tree = parser.parse(source) - root = tree.root_node - except Exception as e: - return {"nodes": [], "edges": [], "error": str(e)} - - stem = _file_stem(path) - str_path = str(path) - nodes: list[dict] = [] - edges: list[dict] = [] - seen_ids: set[str] = set() - function_bodies: list[tuple[str, object]] = [] - - def add_node(nid: str, label: str, line: int) -> None: - if nid not in seen_ids: - seen_ids.add(nid) - nodes.append({ - "id": nid, - "label": label, - "file_type": "code", - "source_file": str_path, - "source_location": f"L{line}", - }) - - def add_edge(src: str, tgt: str, relation: str, line: int, - confidence: str = "EXTRACTED", weight: float = 1.0, - context: str | None = None) -> None: - edge = { - "source": src, - "target": tgt, - "relation": relation, - "confidence": confidence, - "source_file": str_path, - "source_location": f"L{line}", - "weight": weight, - } - if context: - edge["context"] = context - edges.append(edge) - - file_nid = _make_id(str(path)) - add_node(file_nid, path.name, 1) - - def ensure_named_node(name: str, line: int) -> str: - nid = _make_id(stem, name) - if nid in seen_ids: - return nid - nid = _make_id(name) - if nid not in seen_ids: - # The name isn't defined in this file, so this is a cross-file reference - # (e.g. a `Thing` type annotation imported from another module). Emit a - # SOURCELESS stub — like the inheritance-base path below — so the - # corpus-level rewire can collapse it onto the real definition. A sourced - # stub here makes _disambiguate_colliding_node_ids bake the referencing - # file's path (with extension) into the id and blocks the rewire, which is - # the phantom-duplicate-node bug (#1402). - seen_ids.add(nid) - nodes.append({ - "id": nid, - "label": name, - "file_type": "code", - "source_file": "", - "source_location": "", - "origin_file": str_path, - }) - return nid - - def emit_param_return_refs(func_node, func_nid: str, line: int) -> None: - params = func_node.child_by_field_name("parameters") - if params is not None: - for p in params.children: - if p.type != "parameter": - continue - type_node = p.child_by_field_name("type") - refs: list[tuple[str, str]] = [] - _rust_collect_type_refs(type_node, source, False, refs) - for ref_name, role in refs: - ctx = "generic_arg" if role == "generic_arg" else "parameter_type" - tgt = ensure_named_node(ref_name, line) - if tgt != func_nid: - add_edge(func_nid, tgt, "references", line, context=ctx) - return_type = func_node.child_by_field_name("return_type") - if return_type is not None: - refs = [] - _rust_collect_type_refs(return_type, source, False, refs) - for ref_name, role in refs: - ctx = "generic_arg" if role == "generic_arg" else "return_type" - tgt = ensure_named_node(ref_name, line) - if tgt != func_nid: - add_edge(func_nid, tgt, "references", line, context=ctx) - - def walk(node, parent_impl_nid: str | None = None) -> None: - t = node.type - - if t == "function_item": - name_node = node.child_by_field_name("name") - if name_node: - func_name = _read_text(name_node, source) - line = node.start_point[0] + 1 - if parent_impl_nid: - func_nid = _make_id(parent_impl_nid, func_name) - add_node(func_nid, f".{func_name}()", line) - add_edge(parent_impl_nid, func_nid, "method", line) - else: - func_nid = _make_id(stem, func_name) - add_node(func_nid, f"{func_name}()", line) - add_edge(file_nid, func_nid, "contains", line) - emit_param_return_refs(node, func_nid, line) - body = node.child_by_field_name("body") - if body: - function_bodies.append((func_nid, body)) - return - - if t in ("struct_item", "enum_item", "trait_item"): - name_node = node.child_by_field_name("name") - if name_node: - item_name = _read_text(name_node, source) - line = node.start_point[0] + 1 - item_nid = _make_id(stem, item_name) - add_node(item_nid, item_name, line) - add_edge(file_nid, item_nid, "contains", line) - if t == "trait_item": - for c in node.children: - if c.type != "trait_bounds": - continue - for sub in c.children: - if not sub.is_named: - continue - refs: list[tuple[str, str]] = [] - _rust_collect_type_refs(sub, source, False, refs) - for idx, (ref_name, _role) in enumerate(refs): - tgt = ensure_named_node(ref_name, line) - if tgt == item_nid: - continue - rel = "inherits" if idx == 0 else "references" - if rel == "inherits": - add_edge(item_nid, tgt, "inherits", line) - else: - add_edge(item_nid, tgt, "references", line, - context="generic_arg") - if t == "struct_item": - for c in node.children: - if c.type != "field_declaration_list": - continue - for field in c.children: - if field.type != "field_declaration": - continue - type_node = field.child_by_field_name("type") - if type_node is None: - for fc in field.children: - if fc.type in ("type_identifier", "generic_type", - "scoped_type_identifier", - "reference_type", "primitive_type"): - type_node = fc - break - refs = [] - _rust_collect_type_refs(type_node, source, False, refs) - for ref_name, role in refs: - ctx = "generic_arg" if role == "generic_arg" else "field" - tgt = ensure_named_node(ref_name, field.start_point[0] + 1) - if tgt != item_nid: - add_edge(item_nid, tgt, "references", - field.start_point[0] + 1, context=ctx) - return - - if t == "impl_item": - type_node = node.child_by_field_name("type") - trait_node = node.child_by_field_name("trait") - impl_nid: str | None = None - if type_node: - type_name = _read_text(type_node, source).strip() - impl_nid = _make_id(stem, type_name) - add_node(impl_nid, type_name, node.start_point[0] + 1) - if trait_node is not None and impl_nid is not None: - refs: list[tuple[str, str]] = [] - _rust_collect_type_refs(trait_node, source, False, refs) - for idx, (ref_name, _role) in enumerate(refs): - tgt = ensure_named_node(ref_name, node.start_point[0] + 1) - if tgt == impl_nid: - continue - if idx == 0: - add_edge(impl_nid, tgt, "implements", node.start_point[0] + 1) - else: - add_edge(impl_nid, tgt, "references", node.start_point[0] + 1, - context="generic_arg") - body = node.child_by_field_name("body") - if body: - for child in body.children: - walk(child, parent_impl_nid=impl_nid) - return - - if t == "use_declaration": - arg = node.child_by_field_name("argument") - if arg: - raw = _read_text(arg, source) - clean = raw.split("{")[0].rstrip(":").rstrip("*").rstrip(":") - module_name = clean.split("::")[-1].strip() - if module_name: - tgt_nid = _make_id(module_name) - add_edge(file_nid, tgt_nid, "imports_from", node.start_point[0] + 1, context="import") - return - - for child in node.children: - walk(child, parent_impl_nid=None) - - walk(root) - - label_to_nid: dict[str, str] = {} - for n in nodes: - raw = n["label"] - normalised = raw.strip("()").lstrip(".") - label_to_nid[normalised] = n["id"] - - seen_call_pairs: set[tuple[str, str]] = set() - raw_calls: list[dict] = [] - - def walk_calls(node, caller_nid: str) -> None: - if node.type == "function_item": - return - if node.type == "call_expression": - func_node = node.child_by_field_name("function") - callee_name: str | None = None - is_member_call: bool = False - is_scoped_call: bool = False - if func_node: - if func_node.type == "identifier": - callee_name = _read_text(func_node, source) - elif func_node.type == "field_expression": - is_member_call = True - field = func_node.child_by_field_name("field") - if field: - callee_name = _read_text(field, source) - elif func_node.type == "scoped_identifier": - # Type::method() — still allow in-file EXTRACTED match, but - # skip cross-file resolution: bare last-segment lookup ignores - # crate boundaries and produces spurious INFERRED edges (#908). - is_scoped_call = True - name = func_node.child_by_field_name("name") - if name: - callee_name = _read_text(name, source) - if callee_name and callee_name not in _LANGUAGE_BUILTIN_GLOBALS: - tgt_nid = label_to_nid.get(callee_name) - if tgt_nid and tgt_nid != caller_nid: - pair = (caller_nid, tgt_nid) - if pair not in seen_call_pairs: - seen_call_pairs.add(pair) - line = node.start_point[0] + 1 - edges.append({ - "source": caller_nid, - "target": tgt_nid, - "relation": "calls", - "context": "call", - "confidence": "EXTRACTED", - "source_file": str_path, - "source_location": f"L{line}", - "weight": 1.0, - }) - elif not is_scoped_call and callee_name.lower() not in _RUST_TRAIT_METHOD_BLOCKLIST: - raw_calls.append({ - "caller_nid": caller_nid, - "callee": callee_name, - "is_member_call": is_member_call, - "source_file": str_path, - "source_location": f"L{node.start_point[0] + 1}", - }) - for child in node.children: - walk_calls(child, caller_nid) - - for caller_nid, body_node in function_bodies: - walk_calls(body_node, caller_nid) - - valid_ids = seen_ids - clean_edges = [] - for edge in edges: - src, tgt = edge["source"], edge["target"] - if src in valid_ids and (tgt in valid_ids or edge["relation"] in ("imports", "imports_from")): - clean_edges.append(edge) - - return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls} - - -# ── Zig ─────────────────────────────────────────────────────────────────────── - - - -# ── PowerShell ──────────────────────────────────────────────────────────────── - -def extract_powershell(path: Path) -> dict: - """Extract functions, classes, methods, and using statements from a .ps1 file.""" - try: - import tree_sitter_powershell as tsps - from tree_sitter import Language, Parser - except ImportError: - return {"nodes": [], "edges": [], "error": "tree_sitter_powershell not installed"} - - try: - language = Language(tsps.language()) - parser = Parser(language) - source = path.read_bytes() - tree = parser.parse(source) - root = tree.root_node - except Exception as e: - return {"nodes": [], "edges": [], "error": str(e)} - - stem = _file_stem(path) - str_path = str(path) - nodes: list[dict] = [] - edges: list[dict] = [] - seen_ids: set[str] = set() - function_bodies: list[tuple[str, Any]] = [] - - def add_node(nid: str, label: str, line: int) -> None: - if nid not in seen_ids: - seen_ids.add(nid) - nodes.append({"id": nid, "label": label, "file_type": "code", - "source_file": str_path, "source_location": f"L{line}"}) - - def add_edge(src: str, tgt: str, relation: str, line: int, - confidence: str = "EXTRACTED", weight: float = 1.0, - context: str | None = None) -> None: - edge = {"source": src, "target": tgt, "relation": relation, - "confidence": confidence, "source_file": str_path, - "source_location": f"L{line}", "weight": weight} - if context: - edge["context"] = context - edges.append(edge) - - file_nid = _make_id(str(path)) - add_node(file_nid, path.name, 1) - - _PS_SKIP = frozenset({ - "using", "return", "if", "else", "elseif", "foreach", "for", - "while", "do", "switch", "try", "catch", "finally", "throw", - "break", "continue", "exit", "param", "begin", "process", "end", - # Import commands — handled as import edges, not function calls - "import-module", - }) - - def _find_script_block_body(node): - for child in node.children: - if child.type == "script_block": - for sc in child.children: - if sc.type == "script_block_body": - return sc - return child - return None - - def ensure_named_node(name: str, line: int) -> str: - nid = _make_id(stem, name) - if nid in seen_ids: - return nid - nid = _make_id(name) - if nid not in seen_ids: - # The name isn't defined in this file, so this is a cross-file reference - # (e.g. a `Thing` type annotation imported from another module). Emit a - # SOURCELESS stub — like the inheritance-base path below — so the - # corpus-level rewire can collapse it onto the real definition. A sourced - # stub here makes _disambiguate_colliding_node_ids bake the referencing - # file's path (with extension) into the id and blocks the rewire, which is - # the phantom-duplicate-node bug (#1402). - seen_ids.add(nid) - nodes.append({ - "id": nid, - "label": name, - "file_type": "code", - "source_file": "", - "source_location": "", - "origin_file": str_path, - }) - return nid - - def _ps_type_name(type_literal_node) -> str | None: - """Drill into a type_literal node and return the inner type_identifier text.""" - if type_literal_node is None: - return None - for spec in type_literal_node.children: - if spec.type != "type_spec": - continue - for tname in spec.children: - if tname.type != "type_name": - continue - for tid in tname.children: - if tid.type == "type_identifier": - return _read_text(tid, source) - return None - - def walk(node, parent_class_nid: str | None = None) -> None: - t = node.type - - if t == "function_statement": - name_node = next((c for c in node.children if c.type == "function_name"), None) - if name_node: - func_name = _read_text(name_node, source) - line = node.start_point[0] + 1 - func_nid = _make_id(stem, func_name) - add_node(func_nid, f"{func_name}()", line) - add_edge(file_nid, func_nid, "contains", line) - body = _find_script_block_body(node) - if body: - function_bodies.append((func_nid, body)) - # Also walk the body during the main pass so that - # Import-Module / dot-source inside functions emit - # file-level imports_from edges (#1331). - walk(body, parent_class_nid) - return - - if t == "class_statement": - name_node = next((c for c in node.children if c.type == "simple_name"), None) - if name_node: - class_name = _read_text(name_node, source) - line = node.start_point[0] + 1 - class_nid = _make_id(stem, class_name) - add_node(class_nid, class_name, line) - add_edge(file_nid, class_nid, "contains", line) - for child in node.children: - walk(child, parent_class_nid=class_nid) - return - - if t == "class_property_definition" and parent_class_nid: - type_literal = next((c for c in node.children if c.type == "type_literal"), None) - type_name = _ps_type_name(type_literal) - if type_name: - line = node.start_point[0] + 1 - target_nid = ensure_named_node(type_name, line) - if target_nid != parent_class_nid: - add_edge(parent_class_nid, target_nid, "references", - line, context="field") - return - - if t == "class_method_definition": - name_node = next((c for c in node.children if c.type == "simple_name"), None) - if name_node: - method_name = _read_text(name_node, source) - line = node.start_point[0] + 1 - if parent_class_nid: - method_nid = _make_id(parent_class_nid, method_name) - add_node(method_nid, f".{method_name}()", line) - add_edge(parent_class_nid, method_nid, "method", line) - else: - method_nid = _make_id(stem, method_name) - add_node(method_nid, f"{method_name}()", line) - add_edge(file_nid, method_nid, "contains", line) - # Return type: type_literal sibling of simple_name - return_type_literal = next( - (c for c in node.children if c.type == "type_literal"), None) - return_type_name = _ps_type_name(return_type_literal) - if return_type_name: - target_nid = ensure_named_node(return_type_name, line) - if target_nid != method_nid: - add_edge(method_nid, target_nid, "references", - line, context="return_type") - # Parameter types: class_method_parameter_list - param_list = next( - (c for c in node.children if c.type == "class_method_parameter_list"), None) - if param_list is not None: - for p in param_list.children: - if p.type != "class_method_parameter": - continue - ptype_literal = next( - (c for c in p.children if c.type == "type_literal"), None) - ptype_name = _ps_type_name(ptype_literal) - if not ptype_name: - continue - p_line = p.start_point[0] + 1 - target_nid = ensure_named_node(ptype_name, p_line) - if target_nid != method_nid: - add_edge(method_nid, target_nid, "references", - p_line, context="parameter_type") - body = _find_script_block_body(node) - if body: - function_bodies.append((method_nid, body)) - return - - if t == "command": - # Dot-sourcing: `. ./Shared.psm1` - # Uses command_invokation_operator '.' + command_name_expr (not command_name) - invoke_op = next( - (c for c in node.children if c.type == "command_invokation_operator"), None - ) - if invoke_op is not None and _read_text(invoke_op, source).strip() == ".": - name_expr = next( - (c for c in node.children if c.type == "command_name_expr"), None - ) - if name_expr is not None: - name_node = next( - (c for c in name_expr.children if c.type == "command_name"), None - ) - if name_node: - raw_path = _read_text(name_node, source) - # Strip relative path prefix (./ or .\ or just the dot) - module_stem = re.sub(r'^[./\\]+', '', raw_path) - # Drop extension to get bare module name - module_stem = re.sub(r'\.[^.]+$', '', module_stem).replace('\\', '/') - module_name = module_stem.split('/')[-1] - if module_name: - add_edge(file_nid, _make_id(module_name), "imports_from", - node.start_point[0] + 1) - return - - cmd_name_node = next((c for c in node.children if c.type == "command_name"), None) - if cmd_name_node: - cmd_text = _read_text(cmd_name_node, source).lower() - if cmd_text == "using": - tokens = [] - for child in node.children: - if child.type == "command_elements": - for el in child.children: - if el.type == "generic_token": - tokens.append(_read_text(el, source)) - module_tokens = [t for t in tokens - if t.lower() not in ("namespace", "module", "assembly")] - if module_tokens: - module_name = module_tokens[-1].split(".")[-1] - add_edge(file_nid, _make_id(module_name), "imports_from", - node.start_point[0] + 1) - elif cmd_text == "import-module": - # Collect generic_token args; skip command_parameter flags like -Name - # The module name is the first generic_token (or the one after -Name) - module_name: str | None = None - expect_name = False - for child in node.children: - if child.type != "command_elements": - continue - for el in child.children: - if el.type == "command_parameter": - param_text = _read_text(el, source).lstrip("-").lower() - expect_name = param_text in ("name", "n") - elif el.type == "generic_token": - module_token = _read_text(el, source) - if module_name is None or expect_name: - module_name = module_token - expect_name = False - if module_name: - # Strip extension; keep only the stem for the node ID - bare = re.sub(r'\.[^.]+$', '', module_name).split('/')[-1].split('\\')[-1] - if bare: - add_edge(file_nid, _make_id(bare), "imports_from", - node.start_point[0] + 1) - return - - for child in node.children: - walk(child, parent_class_nid) - - walk(root) - - label_to_nid = {n["label"].strip("()").lstrip(".").lower(): n["id"] for n in nodes} - seen_call_pairs: set[tuple[str, str]] = set() - raw_calls: list[dict] = [] - - def walk_calls(node, caller_nid: str) -> None: - if node.type in ("function_statement", "class_statement"): - return - if node.type == "command": - cmd_name_node = next((c for c in node.children if c.type == "command_name"), None) - if cmd_name_node: - cmd_text = _read_text(cmd_name_node, source) - if cmd_text.lower() not in _PS_SKIP: - tgt_nid = label_to_nid.get(cmd_text.lower()) - if tgt_nid and tgt_nid != caller_nid: - pair = (caller_nid, tgt_nid) - if pair not in seen_call_pairs: - seen_call_pairs.add(pair) - add_edge(caller_nid, tgt_nid, "calls", - node.start_point[0] + 1, - confidence="EXTRACTED", weight=1.0) - elif cmd_text: - raw_calls.append({ - "caller_nid": caller_nid, - "callee": cmd_text, - "is_member_call": False, - "source_file": str_path, - "source_location": f"L{node.start_point[0] + 1}", - }) - for child in node.children: - walk_calls(child, caller_nid) - - for caller_nid, body_node in function_bodies: - walk_calls(body_node, caller_nid) - - clean_edges = [e for e in edges if e["source"] in seen_ids and - (e["target"] in seen_ids or e["relation"] in ("imports_from", "imports"))] - return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls} - - -# ── PowerShell manifest (.psd1) ────────────────────────────────────────────── - -# Keys in a .psd1 whose values are module names/paths we treat as imports. -_PSD1_IMPORT_KEYS = frozenset({"RootModule", "NestedModules", "RequiredModules"}) - - -def _psd1_collect_string_literals(node, source: bytes) -> list[str]: - """Recursively collect all string_literal text values under *node*.""" - results: list[str] = [] - - def _walk(n) -> None: - if n.type == "string_literal": - raw = source[n.start_byte:n.end_byte].decode(errors="replace") - # Strip surrounding quote chars (' or ") - results.append(raw.strip("'\"")) - return - for child in n.children: - _walk(child) - - _walk(node) - return results - - -def _psd1_module_name(raw: str) -> str: - """Derive a bare module name from a raw string value. - - e.g. 'MyModule.psm1' → 'MyModule', './sub/Util.psm1' → 'Util', 'PSReadLine' → 'PSReadLine' - """ - # Strip path prefix and extension - name = raw.replace("\\", "/").split("/")[-1] - name = re.sub(r"\.[^.]+$", "", name) # remove last extension - return name.strip() - - -def extract_powershell_manifest(path: Path) -> dict: - """Extract module dependency edges from a PowerShell .psd1 manifest file. - - .psd1 files are PowerShell data hashtables, not scripts. tree-sitter-powershell - parses them correctly (they are syntactically valid PS). We walk the AST looking - for RootModule, NestedModules, and RequiredModules keys and emit imports_from - edges for every referenced module. - - RequiredModules supports two forms: - - Simple string: 'PSReadLine' - - Module specification: @{ ModuleName = 'Pester'; ModuleVersion = '5.0' } - For the hashtable form we only follow the ModuleName key. - """ - try: - import tree_sitter_powershell as tsps - from tree_sitter import Language, Parser - except ImportError: - return {"nodes": [], "edges": [], "error": "tree_sitter_powershell not installed"} - - try: - language = Language(tsps.language()) - parser = Parser(language) - source = path.read_bytes() - tree = parser.parse(source) - root = tree.root_node - except Exception as e: - return {"nodes": [], "edges": [], "error": str(e)} - - str_path = str(path) - nodes: list[dict] = [] - edges: list[dict] = [] - seen_ids: set[str] = set() - - def add_node(nid: str, label: str, line: int) -> None: - if nid not in seen_ids: - seen_ids.add(nid) - nodes.append({"id": nid, "label": label, "file_type": "code", - "source_file": str_path, "source_location": f"L{line}"}) - - def add_import_edge(src: str, module_raw: str, line: int) -> None: - name = _psd1_module_name(module_raw) - if not name: - return - tgt_nid = _make_id(name) - edges.append({ - "source": src, - "target": tgt_nid, - "relation": "imports_from", - "confidence": "EXTRACTED", - "source_file": str_path, - "source_location": f"L{line}", - "weight": 1.0, - "context": "import", - }) - - file_nid = _make_id(str(path)) - add_node(file_nid, path.name, 1) - - def walk_manifest(node) -> None: - """Walk the AST and emit edges for import-relevant hash_entry nodes.""" - if node.type != "hash_entry": - for child in node.children: - walk_manifest(child) - return - - # Identify the key - key_node = next((c for c in node.children if c.type == "key_expression"), None) - if key_node is None: - return - key_text = source[key_node.start_byte:key_node.end_byte].decode(errors="replace").strip() - - if key_text not in _PSD1_IMPORT_KEYS: - # Still recurse in case there are nested hashes (e.g. ModuleVersion entries - # contain sub-hashes, but we only care about top-level keys for imports) - return - - line = node.start_point[0] + 1 - value_node = next((c for c in node.children if c.type == "pipeline"), None) - if value_node is None: - return - - if key_text == "RootModule": - # Value is a single string - strings = _psd1_collect_string_literals(value_node, source) - for s in strings: - add_import_edge(file_nid, s, line) - - elif key_text == "NestedModules": - # Value is a string or @('a', 'b', ...) array — collect all string literals - strings = _psd1_collect_string_literals(value_node, source) - for s in strings: - add_import_edge(file_nid, s, line) - - elif key_text == "RequiredModules": - # Two forms: - # 1) 'SimpleModule' — direct string literals in the array - # 2) @{ ModuleName = 'Foo'; ModuleVersion = '2.0' } — use ModuleName only - # - # Strategy: walk the value for hash_entry nodes whose key is 'ModuleName'; - # collect their string values. For the remaining string_literal nodes that - # are NOT inside a hash_entry subtree, treat them as simple module names. - module_name_strings: list[str] = [] - inside_hash_entries: set[int] = set() # byte offsets of handled strings - - def find_modulename_entries(n) -> None: - if n.type == "hash_entry": - sub_key = next((c for c in n.children if c.type == "key_expression"), None) - if sub_key is not None: - sk_text = source[sub_key.start_byte:sub_key.end_byte].decode(errors="replace").strip() - # Collect strings inside *all* sub-keys so we can exclude them - for c in n.children: - if c.type == "pipeline": - for s_node in _collect_string_nodes(c): - inside_hash_entries.add(s_node.start_byte) - if sk_text == "ModuleName": - for c in n.children: - if c.type == "pipeline": - for s in _psd1_collect_string_literals(c, source): - module_name_strings.append(s) - return # don't recurse further into this hash_entry - for child in n.children: - find_modulename_entries(child) - - def _collect_string_nodes(n): - """Return all string_literal nodes in subtree.""" - if n.type == "string_literal": - yield n - return - for child in n.children: - yield from _collect_string_nodes(child) - - find_modulename_entries(value_node) - - # Now gather direct string literals not inside hash entries - direct_strings: list[str] = [] - for s_node in _collect_string_nodes(value_node): - if s_node.start_byte not in inside_hash_entries: - raw = source[s_node.start_byte:s_node.end_byte].decode(errors="replace") - direct_strings.append(raw.strip("'\"")) - - for s in direct_strings + module_name_strings: - add_import_edge(file_nid, s, line) - - walk_manifest(root) - - return {"nodes": nodes, "edges": edges, "raw_calls": []} - - -# ── Cross-file import resolution ────────────────────────────────────────────── - -def _source_key(source_file: str, root: Path) -> str: - if not source_file: - return "" - source_path = Path(source_file) - try: - return str(source_path.resolve().relative_to(root)) - except Exception: - return str(source_path) - - -def _node_disambiguation_source_key(node: dict, root: Path) -> str: - source_file = str(node.get("source_file", "")) - if source_file: - return _source_key(source_file, root) - return _source_key(str(node.get("origin_file", "")), root) - - -def _disambiguate_colliding_node_ids( - nodes: list[dict], - edges: list[dict], - raw_calls: list[dict], - root: Path, -) -> None: - """Rewrite only colliding node IDs, using source path as the disambiguator. - - Module anchor nodes (#1327) are exempt: ``import CoreKit`` from three files - yields three ``type=module`` nodes with the same id but different - source_files. Those are the *same* module, not distinct same-named symbols, - so they must collapse to one shared node — disambiguating them by path would - scatter a single module across N file-qualified duplicates. - """ - by_id: dict[str, list[dict]] = {} - for node in nodes: - if node.get("type") == "module": - continue - nid = node.get("id") - if isinstance(nid, str) and nid: - by_id.setdefault(nid, []).append(node) - - remap: dict[tuple[str, str], str] = {} - ambiguous_ids: set[str] = set() - for old_id, group in by_id.items(): - source_keys = {_node_disambiguation_source_key(node, root) for node in group} - if len(group) < 2 or len(source_keys) < 2: - continue - ambiguous_ids.add(old_id) - # Salt the colliding id with the *path* it came from. The naive salt is - # ``_make_id(source_key, old_id)`` — source_key is the raw repo-relative - # path. But _make_id collapses every separator, so two DISTINCT paths - # whose only difference is a separator-vs-inner-punctuation swap - # (``a/b/c.md`` vs ``a.b/c.md``, ``foo/bar_baz.md`` vs ``foo_bar/baz.md``) - # normalize to the SAME salted id and still collide (#1522 — the residual - # of #1504 the 0.9.0 full-path stem didn't reach). When that happens, - # append a short stable hash of the *raw* source_key, which IS injective - # over distinct paths, so the colliders separate. Computed in code from - # source_file (never trusted from the LLM), so AST↔semantic parity holds. - naive: dict[str, str] = {} # source_key -> _make_id(source_key, old_id) - for source_key in source_keys: - if source_key: - naive[source_key] = _make_id(source_key, old_id) - # source_keys that, after normalization, are not unique among themselves. - seen: dict[str, int] = {} - for nid in naive.values(): - seen[nid] = seen.get(nid, 0) + 1 - needs_hash = {sk for sk, nid in naive.items() if seen.get(nid, 0) > 1} - for node in group: - source_key = _node_disambiguation_source_key(node, root) - if not source_key: - continue - if source_key in needs_hash: - salt = hashlib.sha1(source_key.encode("utf-8")).hexdigest()[:6] - new_id = _make_id(source_key, old_id, salt) - else: - new_id = naive.get(source_key) or _make_id(source_key, old_id) - remap[(old_id, source_key)] = new_id - if new_id != old_id: - node["id"] = new_id - - if not remap: - return - - unambiguous_remaps: dict[str, str] = {} - for old_id, group in by_id.items(): - if old_id in ambiguous_ids: - continue - candidates = { - node["id"] for node in group - if isinstance(node.get("id"), str) and node["id"] != old_id - } - if len(candidates) == 1: - unambiguous_remaps[old_id] = next(iter(candidates)) - - # A C/ObjC/C++ `#include "foo.h"` / `#import "foo.h"` resolves to the header's - # file node, but `foo.h` and its sibling `foo.c`/`foo.m`/`foo.cpp` collapse to - # the same `foo` file id, so disambiguation salts them apart by path. A - # cross-file import edge from a THIRD file carries neither salt's source_key, so - # the (target, edge_source_key) lookup misses and the edge dangles on the now - # dead `foo` id. Repoint those import edges to the HEADER variant (the include - # always targeted the header), keyed by the original colliding id (#1475). - _HEADER_SUFFIXES = (".h", ".hpp", ".hh", ".hxx") - header_remaps: dict[str, str] = {} - for old_id in ambiguous_ids: - for node in by_id.get(old_id, []): - sk = _node_disambiguation_source_key(node, root) - if sk and Path(sk).suffix.lower() in _HEADER_SUFFIXES: - new_id = remap.get((old_id, sk)) - if new_id: - header_remaps[old_id] = new_id - break - - for edge in edges: - edge_source_key = _source_key(str(edge.get("source_file", "")), root) - source_key = (edge.get("source", ""), edge_source_key) - target_key = (edge.get("target", ""), edge_source_key) - if source_key in remap: - edge["source"] = remap[source_key] - elif edge.get("source") in unambiguous_remaps: - edge["source"] = unambiguous_remaps[str(edge["source"])] - # imports/imports_from always target a header file, so they must resolve to - # the header variant BEFORE the same-source-file salt is considered. Keying - # the import target by the importer's own source file mis-points a `.m` - # importing its own `.h` back at itself (self-loop), and is wrong for any - # cross-file import whose importer shares the colliding id (#1475). - if (edge.get("relation") in ("imports", "imports_from") - and edge.get("target") in header_remaps): - edge["target"] = header_remaps[str(edge["target"])] - elif target_key in remap: - edge["target"] = remap[target_key] - elif edge.get("target") in unambiguous_remaps: - edge["target"] = unambiguous_remaps[str(edge["target"])] - - for raw_call in raw_calls: - call_source_key = _source_key(str(raw_call.get("source_file", "")), root) - caller_key = (raw_call.get("caller_nid", ""), call_source_key) - if caller_key in remap: - raw_call["caller_nid"] = remap[caller_key] - elif raw_call.get("caller_nid") in unambiguous_remaps: - raw_call["caller_nid"] = unambiguous_remaps[str(raw_call["caller_nid"])] - - -def _node_label_key(node: dict) -> str: - label = str(node.get("label", "")).strip() - return re.sub(r"[^a-zA-Z0-9]+", "", label).lower() - - -def _is_type_like_definition(node: dict) -> bool: - label = str(node.get("label", "")).strip() - if not label: - return False - if label.endswith(")") or label.startswith("."): - return False - if "." in label: - return False - return node.get("file_type") == "code" - - -def _rewire_unique_stub_nodes(nodes: list[dict], edges: list[dict]) -> None: - """Map unresolved no-source stubs to a unique real definition with the same label.""" - real_by_label: dict[str, list[dict]] = {} - stubs: list[dict] = [] - - for node in nodes: - key = _node_label_key(node) - if not key: - continue - if node.get("source_file"): - if _is_type_like_definition(node): - real_by_label.setdefault(key, []).append(node) - continue - stubs.append(node) - - remap: dict[str, str] = {} - drop_ids: set[str] = set() - for stub in stubs: - stub_id = str(stub.get("id", "")) - if not stub_id: - continue - candidates = real_by_label.get(_node_label_key(stub), []) - if len(candidates) != 1: - continue - target_id = candidates[0].get("id") - if isinstance(target_id, str) and target_id and target_id != stub_id: - remap[stub_id] = target_id - drop_ids.add(stub_id) - - if not remap: - return - - for edge in edges: - if edge.get("source") in remap: - edge["source"] = remap[str(edge["source"])] - if edge.get("target") in remap: - edge["target"] = remap[str(edge["target"])] - - nodes[:] = [node for node in nodes if node.get("id") not in drop_ids] - - -def _js_source_path(source_file: str, root: Path) -> Path | None: - if not source_file: - return None - path = Path(source_file) - if not path.is_absolute(): - path = root / path - try: - return path.resolve() - except Exception: - return path - - -@dataclass(frozen=True) -class _SymbolDeclarationFact: - file_path: Path - name: str - line: int - - -@dataclass(frozen=True) -class _SymbolImportFact: - file_path: Path - local_name: str - target_path: Path - imported_name: str - line: int - - -@dataclass(frozen=True) -class _SymbolAliasFact: - file_path: Path - alias: str - target_name: str - line: int - - -@dataclass(frozen=True) -class _SymbolExportFact: - file_path: Path - exported_name: str - line: int - local_name: str | None = None - target_path: Path | None = None - target_name: str | None = None - - -@dataclass(frozen=True) -class _StarExportFact: - file_path: Path - target_path: Path - line: int - - -@dataclass(frozen=True) -class _SymbolUseFact: - file_path: Path - source_id: str - local_name: str - relation: str - context: str - line: int - - -@dataclass -class _SymbolResolutionFacts: - declarations: list[_SymbolDeclarationFact] = field(default_factory=list) - imports: list[_SymbolImportFact] = field(default_factory=list) - aliases: list[_SymbolAliasFact] = field(default_factory=list) - exports: list[_SymbolExportFact] = field(default_factory=list) - star_exports: list[_StarExportFact] = field(default_factory=list) - uses: list[_SymbolUseFact] = field(default_factory=list) - # File-to-file submodule imports from `from pkg import submod` (#1146). - # Each entry is (importing_file, submodule_file, line). - module_imports: list[tuple[Path, Path, int]] = field(default_factory=list) - - -def _apply_symbol_resolution_facts( - paths: list[Path], - nodes: list[dict], - edges: list[dict], - root: Path, - facts: _SymbolResolutionFacts, -) -> None: - """Apply language-provided import/export/use facts to graph edges.""" - if not ( - facts.declarations - or facts.imports - or facts.aliases - or facts.exports - or facts.star_exports - or facts.uses - or facts.module_imports - ): - return - - path_by_resolved = {path.resolve(): path for path in paths} - source_file_id = {path.resolve(): _make_id(str(path)) for path in paths} - symbol_nodes: dict[tuple[Path, str], str] = {} - for node in nodes: - source_path = _js_source_path(str(node.get("source_file", "")), root) - if source_path is None: - continue - label = str(node.get("label", "")).strip().strip("()").lstrip(".") - if label and node.get("id"): - symbol_nodes[(source_path, label)] = str(node["id"]) - - def ensure_symbol_node(path: Path, name: str, line: int) -> str: - resolved_path = path.resolve() - existing = symbol_nodes.get((resolved_path, name)) - if existing is not None: - return existing - node_id = _make_id(_file_stem(path), name) - symbol_nodes[(resolved_path, name)] = node_id - nodes.append({ - "id": node_id, - "label": name, - "file_type": "code", - "source_file": str(path), - "source_location": f"L{line}", - }) - return node_id - - existing_edges = { - ( - str(edge.get("source")), - str(edge.get("target")), - str(edge.get("relation")), - str(edge.get("context") or ""), - ) - for edge in edges - } - - def add_edge(source: str, target: str, relation: str, context: str, line: int, source_path: Path) -> None: - key = (source, target, relation, context or "") - if key in existing_edges: - return - existing_edges.add(key) - edges.append({ - "source": source, - "target": target, - "relation": relation, - "context": context, - "confidence": "EXTRACTED", - "source_file": str(source_path), - "source_location": f"L{line}", - "weight": 1.0, - }) - - for declaration in facts.declarations: - ensure_symbol_node(declaration.file_path, declaration.name, declaration.line) - - local_aliases_by_file: dict[Path, dict[str, tuple[Path, str]]] = {} - for import_fact in facts.imports: - file_path = import_fact.file_path.resolve() - local_aliases_by_file.setdefault(file_path, {})[import_fact.local_name] = ( - import_fact.target_path.resolve(), - import_fact.imported_name, - ) - - pending_aliases_by_file: dict[Path, list[_SymbolAliasFact]] = {} - for alias_fact in facts.aliases: - pending_aliases_by_file.setdefault(alias_fact.file_path.resolve(), []).append(alias_fact) - - for file_path, aliases in pending_aliases_by_file.items(): - local_aliases = local_aliases_by_file.setdefault(file_path, {}) - changed = True - while changed: - changed = False - for alias_fact in aliases: - if alias_fact.alias in local_aliases: - continue - origin = local_aliases.get(alias_fact.target_name) - if origin is not None: - local_aliases[alias_fact.alias] = origin - changed = True - - named_exports_by_file: dict[Path, dict[str, tuple[Path, str]]] = {} - star_exports_by_file: dict[Path, list[Path]] = {} - - for star_fact in facts.star_exports: - source_path = star_fact.file_path.resolve() - target_path = star_fact.target_path.resolve() - star_exports_by_file.setdefault(source_path, []).append(target_path) - source_id = source_file_id.get(source_path) - if source_id is not None: - add_edge( - source_id, - _make_id(str(path_by_resolved.get(target_path, target_path))), - "re_exports", - "export", - star_fact.line, - star_fact.file_path, - ) - - for export_fact in facts.exports: - file_path = export_fact.file_path.resolve() - origin: tuple[Path, str] | None = None - if export_fact.target_path is not None and export_fact.target_name is not None: - origin = (export_fact.target_path.resolve(), export_fact.target_name) - elif export_fact.local_name is not None: - origin = local_aliases_by_file.get(file_path, {}).get(export_fact.local_name) - if origin is None and (file_path, export_fact.local_name) in symbol_nodes: - origin = (file_path, export_fact.local_name) - if origin is None: - continue - named_exports_by_file.setdefault(file_path, {})[export_fact.exported_name] = origin - if origin[0] != file_path: - source_id = source_file_id.get(file_path) - if source_id is not None: - add_edge( - source_id, - _make_id(str(path_by_resolved.get(origin[0], origin[0]))), - "re_exports", - "export", - export_fact.line, - export_fact.file_path, - ) - - def resolve_exported_origin(target_path: Path, imported_name: str, seen: set[tuple[Path, str]] | None = None) -> tuple[Path, str]: - target_path = target_path.resolve() - key = (target_path, imported_name) - if seen is None: - seen = set() - if key in seen: - return key - seen.add(key) - origin = named_exports_by_file.get(target_path, {}).get(imported_name) - if origin is not None: - return resolve_exported_origin(origin[0], origin[1], seen) - for star_target in star_exports_by_file.get(target_path, []): - star_key = (star_target, imported_name) - if star_key in symbol_nodes: - return star_key - resolved = resolve_exported_origin(star_target, imported_name, seen) - if resolved in symbol_nodes: - return resolved - return key - - for import_fact in facts.imports: - source_id = source_file_id.get(import_fact.file_path.resolve()) - if source_id is None: - continue - origin_path, origin_symbol = resolve_exported_origin( - import_fact.target_path, - import_fact.imported_name, - ) - target_id = symbol_nodes.get((origin_path, origin_symbol)) - if target_id is None: - continue - add_edge( - source_id, - target_id, - "imports", - "import", - import_fact.line, - import_fact.file_path, - ) - - # #1146: emit file-to-file imports_from edges for package-form submodule imports. - for from_path, to_path, line in facts.module_imports: - try: - from_rel = from_path.relative_to(root) - to_rel = to_path.relative_to(root) - except ValueError: - continue - source_id = _make_id(_file_stem(from_rel)) - target_id = _make_id(_file_stem(to_rel)) - add_edge(source_id, target_id, "imports_from", "submodule_import", line, from_path) - - for use_fact in facts.uses: - file_path = use_fact.file_path.resolve() - target_id = None - unresolved_origin = local_aliases_by_file.get(file_path, {}).get(use_fact.local_name) - if unresolved_origin is not None: - origin_path, origin_symbol = resolve_exported_origin(*unresolved_origin) - target_id = symbol_nodes.get((origin_path, origin_symbol)) - if target_id is None and use_fact.relation in ("inherits", "implements"): - # Same-file fallback for HERITAGE only: a base declared in the same - # file (`class X extends Y`, `interface A extends B`) has no import - # alias, so resolve it directly against the file's own symbol nodes. - # Scoped to heritage because same-file calls/uses already resolve via - # the dedicated call-graph pass; widening this would duplicate those - # edges. Import resolution still takes precedence (#1095). - target_id = symbol_nodes.get((file_path, use_fact.local_name)) - if target_id is None: - continue - add_edge( - use_fact.source_id, - target_id, - use_fact.relation, - use_fact.context, - use_fact.line, - use_fact.file_path, - ) - - -def _parse_js_tree(path: Path): - try: - from tree_sitter import Language, Parser - # .vue embeds the script in non-JS markup; mask it out and parse the - # close tag + pos = m.end() + if lang is None: + lang_m = _VUE_SCRIPT_LANG_RE.search(m.group(1)) + if lang_m: + lang = lang_m.group(1).lower() + out.append(_blank(src[pos:])) + return "".join(out), lang + +def _source_key(source_file: str, root: Path) -> str: + if not source_file: + return "" + source_path = Path(source_file) + try: + return str(source_path.resolve().relative_to(root)) + except Exception: + return str(source_path) + +def _node_disambiguation_source_key(node: dict, root: Path) -> str: + source_file = str(node.get("source_file", "")) + if source_file: + return _source_key(source_file, root) + return _source_key(str(node.get("origin_file", "")), root) + +def _disambiguate_colliding_node_ids( + nodes: list[dict], + edges: list[dict], + raw_calls: list[dict], + root: Path, +) -> None: + """Rewrite only colliding node IDs, using source path as the disambiguator. + + Module anchor nodes (#1327) are exempt: ``import CoreKit`` from three files + yields three ``type=module`` nodes with the same id but different + source_files. Those are the *same* module, not distinct same-named symbols, + so they must collapse to one shared node — disambiguating them by path would + scatter a single module across N file-qualified duplicates. + """ + by_id: dict[str, list[dict]] = {} + for node in nodes: + if node.get("type") in ("module", "namespace"): + continue + nid = node.get("id") + if isinstance(nid, str) and nid: + by_id.setdefault(nid, []).append(node) + + remap: dict[tuple[str, str], str] = {} + ambiguous_ids: set[str] = set() + for old_id, group in by_id.items(): + source_keys = {_node_disambiguation_source_key(node, root) for node in group} + if len(group) < 2 or len(source_keys) < 2: + continue + ambiguous_ids.add(old_id) + # Salt the colliding id with the *path* it came from. The naive salt is + # ``_make_id(source_key, old_id)`` — source_key is the raw repo-relative + # path. But _make_id collapses every separator, so two DISTINCT paths + # whose only difference is a separator-vs-inner-punctuation swap + # (``a/b/c.md`` vs ``a.b/c.md``, ``foo/bar_baz.md`` vs ``foo_bar/baz.md``) + # normalize to the SAME salted id and still collide (#1522 — the residual + # of #1504 the 0.9.0 full-path stem didn't reach). When that happens, + # append a short stable hash of the *raw* source_key, which IS injective + # over distinct paths, so the colliders separate. Computed in code from + # source_file (never trusted from the LLM), so AST↔semantic parity holds. + naive: dict[str, str] = {} # source_key -> _make_id(source_key, old_id) + for source_key in source_keys: + if source_key: + naive[source_key] = _make_id(source_key, old_id) + # source_keys that, after normalization, are not unique among themselves. + seen: dict[str, int] = {} + for nid in naive.values(): + seen[nid] = seen.get(nid, 0) + 1 + needs_hash = {sk for sk, nid in naive.items() if seen.get(nid, 0) > 1} + for node in group: + source_key = _node_disambiguation_source_key(node, root) + if not source_key: + continue + if source_key in needs_hash: + salt = hashlib.sha1(source_key.encode("utf-8")).hexdigest()[:6] + new_id = _make_id(source_key, old_id, salt) + else: + new_id = naive.get(source_key) or _make_id(source_key, old_id) + remap[(old_id, source_key)] = new_id + if new_id != old_id: + node["id"] = new_id + + if not remap: + return + + unambiguous_remaps: dict[str, str] = {} + for old_id, group in by_id.items(): + if old_id in ambiguous_ids: + continue + candidates = { + node["id"] for node in group + if isinstance(node.get("id"), str) and node["id"] != old_id + } + if len(candidates) == 1: + unambiguous_remaps[old_id] = next(iter(candidates)) + + # A C/ObjC/C++ `#include "foo.h"` / `#import "foo.h"` resolves to the header's + # file node, but `foo.h` and its sibling `foo.c`/`foo.m`/`foo.cpp` collapse to + # the same `foo` file id, so disambiguation salts them apart by path. A + # cross-file import edge from a THIRD file carries neither salt's source_key, so + # the (target, edge_source_key) lookup misses and the edge dangles on the now + # dead `foo` id. Repoint those import edges to the HEADER variant (the include + # always targeted the header), keyed by the original colliding id (#1475). + _HEADER_SUFFIXES = (".h", ".hpp", ".hh", ".hxx") + header_remaps: dict[str, str] = {} + for old_id in ambiguous_ids: + for node in by_id.get(old_id, []): + sk = _node_disambiguation_source_key(node, root) + if sk and Path(sk).suffix.lower() in _HEADER_SUFFIXES: + new_id = remap.get((old_id, sk)) + if new_id: + header_remaps[old_id] = new_id + break + + for edge in edges: + edge_source_key = _source_key(str(edge.get("source_file", "")), root) + source_key = (edge.get("source", ""), edge_source_key) + target_key = (edge.get("target", ""), edge_source_key) + if source_key in remap: + edge["source"] = remap[source_key] + elif edge.get("source") in unambiguous_remaps: + edge["source"] = unambiguous_remaps[str(edge["source"])] + # imports/imports_from always target a header file, so they must resolve to + # the header variant BEFORE the same-source-file salt is considered. Keying + # the import target by the importer's own source file mis-points a `.m` + # importing its own `.h` back at itself (self-loop), and is wrong for any + # cross-file import whose importer shares the colliding id (#1475). + if (edge.get("relation") in ("imports", "imports_from") + and edge.get("target") in header_remaps): + edge["target"] = header_remaps[str(edge["target"])] + elif target_key in remap: + edge["target"] = remap[target_key] + elif edge.get("target") in unambiguous_remaps: + edge["target"] = unambiguous_remaps[str(edge["target"])] + + for raw_call in raw_calls: + call_source_key = _source_key(str(raw_call.get("source_file", "")), root) + caller_key = (raw_call.get("caller_nid", ""), call_source_key) + if caller_key in remap: + raw_call["caller_nid"] = remap[caller_key] + elif raw_call.get("caller_nid") in unambiguous_remaps: + raw_call["caller_nid"] = unambiguous_remaps[str(raw_call["caller_nid"])] + +def _is_type_like_definition(node: dict) -> bool: + if node.get("type") == "namespace": + return False + label = str(node.get("label", "")).strip() + if not label: + return False + if label.endswith(")") or label.startswith("."): + return False + if "." in label: + return False + return node.get("file_type") == "code" + +def _js_source_path(source_file: str, root: Path) -> Path | None: + if not source_file: + return None + path = Path(source_file) + if not path.is_absolute(): + path = root / path + try: + return path.resolve() + except Exception: + return path + +def _apply_symbol_resolution_facts( + paths: list[Path], + nodes: list[dict], + edges: list[dict], + root: Path, + facts: _SymbolResolutionFacts, +) -> None: + """Apply language-provided import/export/use facts to graph edges.""" + if not ( + facts.declarations + or facts.imports + or facts.aliases + or facts.exports + or facts.star_exports + or facts.namespace_exports + or facts.uses + or facts.module_imports + ): + return + + path_by_resolved = {path.resolve(): path for path in paths} + source_file_id = {path.resolve(): _make_id(str(path)) for path in paths} + symbol_nodes: dict[tuple[Path, str], str] = {} + for node in nodes: + source_path = _js_source_path(str(node.get("source_file", "")), root) + if source_path is None: + continue + label = str(node.get("label", "")).strip().strip("()").lstrip(".") + if label and node.get("id"): + symbol_nodes[(source_path, label)] = str(node["id"]) + + def ensure_symbol_node(path: Path, name: str, line: int) -> str: + resolved_path = path.resolve() + existing = symbol_nodes.get((resolved_path, name)) + if existing is not None: + return existing + node_id = _make_id(_file_stem(path), name) + symbol_nodes[(resolved_path, name)] = node_id + nodes.append({ + "id": node_id, + "label": name, + "file_type": "code", + "source_file": str(path), + "source_location": f"L{line}", + }) + return node_id + + existing_edges = { + ( + str(edge.get("source")), + str(edge.get("target")), + str(edge.get("relation")), + str(edge.get("context") or ""), + ) + for edge in edges + } + + def add_edge(source: str, target: str, relation: str, context: str, line: int, source_path: Path) -> None: + key = (source, target, relation, context or "") + if key in existing_edges: + return + existing_edges.add(key) + edges.append({ + "source": source, + "target": target, + "relation": relation, + "context": context, + "confidence": "EXTRACTED", + "source_file": str(source_path), + "source_location": f"L{line}", + "weight": 1.0, + }) + + for declaration in facts.declarations: + ensure_symbol_node(declaration.file_path, declaration.name, declaration.line) + + local_aliases_by_file: dict[Path, dict[str, tuple[Path, str]]] = {} + for import_fact in facts.imports: + file_path = import_fact.file_path.resolve() + local_aliases_by_file.setdefault(file_path, {})[import_fact.local_name] = ( + import_fact.target_path.resolve(), + import_fact.imported_name, + ) + + pending_aliases_by_file: dict[Path, list[_SymbolAliasFact]] = {} + for alias_fact in facts.aliases: + pending_aliases_by_file.setdefault(alias_fact.file_path.resolve(), []).append(alias_fact) + + for file_path, aliases in pending_aliases_by_file.items(): + local_aliases = local_aliases_by_file.setdefault(file_path, {}) + changed = True + while changed: + changed = False + for alias_fact in aliases: + if alias_fact.alias in local_aliases: + continue + origin = local_aliases.get(alias_fact.target_name) + if origin is not None: + local_aliases[alias_fact.alias] = origin + changed = True + + named_exports_by_file: dict[Path, dict[str, tuple[Path, str]]] = {} + star_exports_by_file: dict[Path, list[Path]] = {} + + for star_fact in facts.star_exports: + source_path = star_fact.file_path.resolve() + target_path = star_fact.target_path.resolve() + star_exports_by_file.setdefault(source_path, []).append(target_path) + source_id = source_file_id.get(source_path) + if source_id is not None: + add_edge( + source_id, + _make_id(str(path_by_resolved.get(target_path, target_path))), + "re_exports", + "export", + star_fact.line, + star_fact.file_path, + ) + + for namespace_fact in facts.namespace_exports: + source_path = namespace_fact.file_path.resolve() + target_path = namespace_fact.target_path.resolve() + namespace_id = ensure_symbol_node( + namespace_fact.file_path, + namespace_fact.exported_name, + namespace_fact.line, + ) + named_exports_by_file.setdefault(source_path, {})[ + namespace_fact.exported_name + ] = (source_path, namespace_fact.exported_name) + source_id = source_file_id.get(source_path) + if source_id is not None: + add_edge( + source_id, + namespace_id, + "contains", + "namespace_export", + namespace_fact.line, + namespace_fact.file_path, + ) + add_edge( + source_id, + _make_id(str(path_by_resolved.get(target_path, target_path))), + "re_exports", + "export", + namespace_fact.line, + namespace_fact.file_path, + ) + + for export_fact in facts.exports: + file_path = export_fact.file_path.resolve() + origin: tuple[Path, str] | None = None + if export_fact.target_path is not None and export_fact.target_name is not None: + origin = (export_fact.target_path.resolve(), export_fact.target_name) + elif export_fact.local_name is not None: + origin = local_aliases_by_file.get(file_path, {}).get(export_fact.local_name) + if origin is None and (file_path, export_fact.local_name) in symbol_nodes: + origin = (file_path, export_fact.local_name) + if origin is None: + continue + named_exports_by_file.setdefault(file_path, {})[export_fact.exported_name] = origin + if origin[0] != file_path: + source_id = source_file_id.get(file_path) + if source_id is not None: + add_edge( + source_id, + _make_id(str(path_by_resolved.get(origin[0], origin[0]))), + "re_exports", + "export", + export_fact.line, + export_fact.file_path, + ) + + def resolve_exported_origin(target_path: Path, imported_name: str, seen: set[tuple[Path, str]] | None = None) -> tuple[Path, str]: + target_path = target_path.resolve() + key = (target_path, imported_name) + if seen is None: + seen = set() + if key in seen: + return key + seen.add(key) + origin = named_exports_by_file.get(target_path, {}).get(imported_name) + if origin is not None: + return resolve_exported_origin(origin[0], origin[1], seen) + for star_target in star_exports_by_file.get(target_path, []): + star_key = (star_target, imported_name) + if star_key in symbol_nodes: + return star_key + resolved = resolve_exported_origin(star_target, imported_name, seen) + if resolved in symbol_nodes: + return resolved + return key + + for import_fact in facts.imports: + source_id = source_file_id.get(import_fact.file_path.resolve()) + if source_id is None: + continue + origin_path, origin_symbol = resolve_exported_origin( + import_fact.target_path, + import_fact.imported_name, + ) + target_id = symbol_nodes.get((origin_path, origin_symbol)) + if target_id is None: + continue + add_edge( + source_id, + target_id, + "imports", + "import", + import_fact.line, + import_fact.file_path, + ) + + # #1146: emit file-to-file imports_from edges for package-form submodule imports. + for from_path, to_path, line in facts.module_imports: + try: + from_rel = from_path.relative_to(root) + to_rel = to_path.relative_to(root) + except ValueError: + continue + source_id = _make_id(_file_stem(from_rel)) + target_id = _make_id(_file_stem(to_rel)) + add_edge(source_id, target_id, "imports_from", "submodule_import", line, from_path) + + for use_fact in facts.uses: + file_path = use_fact.file_path.resolve() + target_id = None + unresolved_origin = local_aliases_by_file.get(file_path, {}).get(use_fact.local_name) + if unresolved_origin is not None: + origin_path, origin_symbol = resolve_exported_origin(*unresolved_origin) + target_id = symbol_nodes.get((origin_path, origin_symbol)) + if target_id is None and use_fact.relation in ("inherits", "implements"): + # Same-file fallback for HERITAGE only: a base declared in the same + # file (`class X extends Y`, `interface A extends B`) has no import + # alias, so resolve it directly against the file's own symbol nodes. + # Scoped to heritage because same-file calls/uses already resolve via + # the dedicated call-graph pass; widening this would duplicate those + # edges. Import resolution still takes precedence (#1095). + target_id = symbol_nodes.get((file_path, use_fact.local_name)) + if target_id is None: + continue + add_edge( + use_fact.source_id, + target_id, + use_fact.relation, + use_fact.context, + use_fact.line, + use_fact.file_path, + ) + +def _parse_js_tree(path: Path): + try: + from tree_sitter import Language, Parser + # .vue embeds the script in non-JS markup; mask it out and parse the + #

uhprpJpDye?Krjt0nHz}Gm#paPJ63e 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