graphitect 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphify/__init__.py +30 -0
- graphify/__main__.py +757 -0
- graphify/_minhash.py +107 -0
- graphify/affected.py +318 -0
- graphify/always_on/agents-md.md +12 -0
- graphify/always_on/antigravity-rules.md +14 -0
- graphify/always_on/claude-md.md +9 -0
- graphify/always_on/gemini-md.md +9 -0
- graphify/always_on/kiro-steering.md +5 -0
- graphify/always_on/vscode-instructions.md +17 -0
- graphify/analyze.py +769 -0
- graphify/benchmark.py +152 -0
- graphify/build.py +2300 -0
- graphify/cache.py +1746 -0
- graphify/callflow_html.py +2051 -0
- graphify/cargo_introspect.py +109 -0
- graphify/cli.py +4745 -0
- graphify/cluster.py +409 -0
- graphify/command-kilo.md +15 -0
- graphify/cross_repo_calls.py +216 -0
- graphify/cross_repo_types.py +75 -0
- graphify/csharp_dispatch.py +154 -0
- graphify/dedup.py +1213 -0
- graphify/detect.py +2566 -0
- graphify/diagnostics.py +406 -0
- graphify/export.py +1349 -0
- graphify/exporters/__init__.py +1 -0
- graphify/exporters/base.py +14 -0
- graphify/exporters/graphdb.py +173 -0
- graphify/exporters/html.py +637 -0
- graphify/extract.py +7856 -0
- graphify/extractors/MIGRATION.md +107 -0
- graphify/extractors/__init__.py +66 -0
- graphify/extractors/apex.py +215 -0
- graphify/extractors/base.py +85 -0
- graphify/extractors/bash.py +579 -0
- graphify/extractors/blade.py +53 -0
- graphify/extractors/commonlisp.py +540 -0
- graphify/extractors/csharp.py +448 -0
- graphify/extractors/dart.py +564 -0
- graphify/extractors/dm.py +494 -0
- graphify/extractors/elixir.py +241 -0
- graphify/extractors/engine.py +6509 -0
- graphify/extractors/fortran.py +311 -0
- graphify/extractors/go.py +527 -0
- graphify/extractors/json_config.py +240 -0
- graphify/extractors/julia.py +289 -0
- graphify/extractors/markdown.py +408 -0
- graphify/extractors/models.py +131 -0
- graphify/extractors/objc.py +566 -0
- graphify/extractors/ocaml.py +289 -0
- graphify/extractors/pascal.py +688 -0
- graphify/extractors/pascal_forms.py +196 -0
- graphify/extractors/powershell.py +522 -0
- graphify/extractors/razor.py +192 -0
- graphify/extractors/resolution.py +3584 -0
- graphify/extractors/robot.py +296 -0
- graphify/extractors/rust.py +470 -0
- graphify/extractors/sln.py +92 -0
- graphify/extractors/sql.py +720 -0
- graphify/extractors/terraform.py +181 -0
- graphify/extractors/verilog.py +329 -0
- graphify/extractors/zig.py +181 -0
- graphify/file_slice.py +246 -0
- graphify/global_graph.py +194 -0
- graphify/google_workspace.py +237 -0
- graphify/hooks.py +933 -0
- graphify/ids.py +93 -0
- graphify/ingest.py +358 -0
- graphify/install.py +2366 -0
- graphify/llm.py +3544 -0
- graphify/manifest.py +4 -0
- graphify/manifest_ingest.py +311 -0
- graphify/mcp_ingest.py +386 -0
- graphify/multigraph_compat.py +212 -0
- graphify/pascal_resolution.py +129 -0
- graphify/paths.py +436 -0
- graphify/pg_introspect.py +165 -0
- graphify/prs.py +770 -0
- graphify/querylog.py +80 -0
- graphify/reflect.py +882 -0
- graphify/report.py +346 -0
- graphify/resolver_registry.py +85 -0
- graphify/ruby_resolution.py +242 -0
- graphify/scip_ingest.py +363 -0
- graphify/security.py +460 -0
- graphify/semantic_cleanup.py +336 -0
- graphify/serve.py +2608 -0
- graphify/skill-agents.md +710 -0
- graphify/skill-aider.md +1283 -0
- graphify/skill-amp.md +710 -0
- graphify/skill-claw.md +713 -0
- graphify/skill-codex.md +710 -0
- graphify/skill-copilot.md +713 -0
- graphify/skill-devin.md +1410 -0
- graphify/skill-droid.md +710 -0
- graphify/skill-kilo.md +722 -0
- graphify/skill-kiro.md +713 -0
- graphify/skill-opencode.md +705 -0
- graphify/skill-pi.md +713 -0
- graphify/skill-trae.md +711 -0
- graphify/skill-vscode.md +709 -0
- graphify/skill-windows.md +755 -0
- graphify/skill.md +713 -0
- graphify/skills/agents/references/add-watch.md +56 -0
- graphify/skills/agents/references/exports.md +87 -0
- graphify/skills/agents/references/extraction-spec.md +70 -0
- graphify/skills/agents/references/github-and-merge.md +46 -0
- graphify/skills/agents/references/hooks.md +33 -0
- graphify/skills/agents/references/query.md +311 -0
- graphify/skills/agents/references/transcribe.md +52 -0
- graphify/skills/agents/references/update.md +210 -0
- graphify/skills/amp/references/add-watch.md +56 -0
- graphify/skills/amp/references/exports.md +87 -0
- graphify/skills/amp/references/extraction-spec.md +70 -0
- graphify/skills/amp/references/github-and-merge.md +46 -0
- graphify/skills/amp/references/hooks.md +33 -0
- graphify/skills/amp/references/query.md +311 -0
- graphify/skills/amp/references/transcribe.md +52 -0
- graphify/skills/amp/references/update.md +210 -0
- graphify/skills/claude/references/add-watch.md +56 -0
- graphify/skills/claude/references/exports.md +87 -0
- graphify/skills/claude/references/extraction-spec.md +70 -0
- graphify/skills/claude/references/github-and-merge.md +46 -0
- graphify/skills/claude/references/hooks.md +33 -0
- graphify/skills/claude/references/query.md +311 -0
- graphify/skills/claude/references/transcribe.md +52 -0
- graphify/skills/claude/references/update.md +210 -0
- graphify/skills/claw/references/add-watch.md +56 -0
- graphify/skills/claw/references/exports.md +87 -0
- graphify/skills/claw/references/extraction-spec.md +31 -0
- graphify/skills/claw/references/github-and-merge.md +46 -0
- graphify/skills/claw/references/hooks.md +33 -0
- graphify/skills/claw/references/query.md +311 -0
- graphify/skills/claw/references/transcribe.md +52 -0
- graphify/skills/claw/references/update.md +210 -0
- graphify/skills/codex/references/add-watch.md +56 -0
- graphify/skills/codex/references/exports.md +87 -0
- graphify/skills/codex/references/extraction-spec.md +31 -0
- graphify/skills/codex/references/github-and-merge.md +46 -0
- graphify/skills/codex/references/hooks.md +33 -0
- graphify/skills/codex/references/query.md +311 -0
- graphify/skills/codex/references/transcribe.md +52 -0
- graphify/skills/codex/references/update.md +210 -0
- graphify/skills/copilot/references/add-watch.md +56 -0
- graphify/skills/copilot/references/exports.md +87 -0
- graphify/skills/copilot/references/extraction-spec.md +70 -0
- graphify/skills/copilot/references/github-and-merge.md +46 -0
- graphify/skills/copilot/references/hooks.md +33 -0
- graphify/skills/copilot/references/query.md +311 -0
- graphify/skills/copilot/references/transcribe.md +52 -0
- graphify/skills/copilot/references/update.md +210 -0
- graphify/skills/droid/references/add-watch.md +56 -0
- graphify/skills/droid/references/exports.md +87 -0
- graphify/skills/droid/references/extraction-spec.md +70 -0
- graphify/skills/droid/references/github-and-merge.md +46 -0
- graphify/skills/droid/references/hooks.md +33 -0
- graphify/skills/droid/references/query.md +311 -0
- graphify/skills/droid/references/transcribe.md +52 -0
- graphify/skills/droid/references/update.md +210 -0
- graphify/skills/kilo/references/add-watch.md +56 -0
- graphify/skills/kilo/references/exports.md +87 -0
- graphify/skills/kilo/references/extraction-spec.md +70 -0
- graphify/skills/kilo/references/github-and-merge.md +46 -0
- graphify/skills/kilo/references/hooks.md +33 -0
- graphify/skills/kilo/references/query.md +311 -0
- graphify/skills/kilo/references/transcribe.md +52 -0
- graphify/skills/kilo/references/update.md +210 -0
- graphify/skills/kiro/references/add-watch.md +56 -0
- graphify/skills/kiro/references/exports.md +87 -0
- graphify/skills/kiro/references/extraction-spec.md +31 -0
- graphify/skills/kiro/references/github-and-merge.md +46 -0
- graphify/skills/kiro/references/hooks.md +33 -0
- graphify/skills/kiro/references/query.md +311 -0
- graphify/skills/kiro/references/transcribe.md +52 -0
- graphify/skills/kiro/references/update.md +210 -0
- graphify/skills/opencode/references/add-watch.md +56 -0
- graphify/skills/opencode/references/exports.md +87 -0
- graphify/skills/opencode/references/extraction-spec.md +70 -0
- graphify/skills/opencode/references/github-and-merge.md +46 -0
- graphify/skills/opencode/references/hooks.md +33 -0
- graphify/skills/opencode/references/query.md +311 -0
- graphify/skills/opencode/references/transcribe.md +52 -0
- graphify/skills/opencode/references/update.md +210 -0
- graphify/skills/pi/references/add-watch.md +56 -0
- graphify/skills/pi/references/exports.md +87 -0
- graphify/skills/pi/references/extraction-spec.md +31 -0
- graphify/skills/pi/references/github-and-merge.md +46 -0
- graphify/skills/pi/references/hooks.md +33 -0
- graphify/skills/pi/references/query.md +311 -0
- graphify/skills/pi/references/transcribe.md +52 -0
- graphify/skills/pi/references/update.md +210 -0
- graphify/skills/trae/references/add-watch.md +56 -0
- graphify/skills/trae/references/exports.md +87 -0
- graphify/skills/trae/references/extraction-spec.md +70 -0
- graphify/skills/trae/references/github-and-merge.md +46 -0
- graphify/skills/trae/references/hooks.md +35 -0
- graphify/skills/trae/references/query.md +311 -0
- graphify/skills/trae/references/transcribe.md +52 -0
- graphify/skills/trae/references/update.md +210 -0
- graphify/skills/vscode/references/add-watch.md +56 -0
- graphify/skills/vscode/references/exports.md +87 -0
- graphify/skills/vscode/references/extraction-spec.md +70 -0
- graphify/skills/vscode/references/github-and-merge.md +46 -0
- graphify/skills/vscode/references/hooks.md +33 -0
- graphify/skills/vscode/references/query.md +311 -0
- graphify/skills/vscode/references/transcribe.md +52 -0
- graphify/skills/vscode/references/update.md +210 -0
- graphify/skills/windows/references/add-watch.md +56 -0
- graphify/skills/windows/references/exports.md +87 -0
- graphify/skills/windows/references/extraction-spec.md +70 -0
- graphify/skills/windows/references/github-and-merge.md +46 -0
- graphify/skills/windows/references/hooks.md +33 -0
- graphify/skills/windows/references/query.md +311 -0
- graphify/skills/windows/references/transcribe.md +52 -0
- graphify/skills/windows/references/update.md +210 -0
- graphify/symbol_resolution.py +556 -0
- graphify/transcribe.py +186 -0
- graphify/tree_html.py +603 -0
- graphify/validate.py +95 -0
- graphify/watch.py +2280 -0
- graphify/wiki.py +405 -0
- graphitect/__init__.py +28 -0
- graphitect/__main__.py +4 -0
- graphitect/_vendor/__init__.py +2 -0
- graphitect/_vendor/archify/LICENSE +22 -0
- graphitect/_vendor/archify/SKILL.md +137 -0
- graphitect/_vendor/archify/THIRD_PARTY_NOTICES.md +69 -0
- graphitect/_vendor/archify/assets/JetBrainsMono-OFL.txt +93 -0
- graphitect/_vendor/archify/assets/template.html +14935 -0
- graphitect/_vendor/archify/bin/archify.mjs +2091 -0
- graphitect/_vendor/archify/bin/open-artifact.mjs +86 -0
- graphitect/_vendor/archify/bin/preview.mjs +653 -0
- graphitect/_vendor/archify/bin/visual-check.mjs +829 -0
- graphitect/_vendor/archify/brand-marks/README.md +31 -0
- graphitect/_vendor/archify/brand-marks/catalog.json +131 -0
- graphitect/_vendor/archify/delta/architecture-delta.mjs +1221 -0
- graphitect/_vendor/archify/examples/agent-run.lifecycle.json +60 -0
- graphitect/_vendor/archify/examples/agent-tool-call.workflow.json +94 -0
- graphitect/_vendor/archify/examples/async-job-roundtrip.sequence.json +61 -0
- graphitect/_vendor/archify/examples/brand-aware-delivery.architecture.json +47 -0
- graphitect/_vendor/archify/examples/cache-miss-request.sequence.json +82 -0
- graphitect/_vendor/archify/examples/checkout-platform.base.architecture.json +31 -0
- graphitect/_vendor/archify/examples/checkout-platform.head.architecture.json +31 -0
- graphitect/_vendor/archify/examples/dataflow-product-analytics.html +15045 -0
- graphitect/_vendor/archify/examples/deployment-release.lifecycle.json +49 -0
- graphitect/_vendor/archify/examples/event-stream.dataflow.json +57 -0
- graphitect/_vendor/archify/examples/incident-response.workflow.json +64 -0
- graphitect/_vendor/archify/examples/lifecycle-agent-run.html +14980 -0
- graphitect/_vendor/archify/examples/product-analytics.dataflow.json +76 -0
- graphitect/_vendor/archify/examples/production-deployment.architecture.json +71 -0
- graphitect/_vendor/archify/examples/release-delivery.workflow.json +62 -0
- graphitect/_vendor/archify/examples/sequence-cache-miss-request.html +15060 -0
- graphitect/_vendor/archify/examples/web-app-rendered.html +15009 -0
- graphitect/_vendor/archify/examples/web-app.architecture.json +46 -0
- graphitect/_vendor/archify/examples/workflow-agent-tool-call-rendered.html +15051 -0
- graphitect/_vendor/archify/migrations/workflow-v2.mjs +279 -0
- graphitect/_vendor/archify/package-lock.json +149 -0
- graphitect/_vendor/archify/package.json +39 -0
- graphitect/_vendor/archify/recipes/scenarios.mjs +391 -0
- graphitect/_vendor/archify/references/authoring-contract.md +243 -0
- graphitect/_vendor/archify/references/brand-marks.md +65 -0
- graphitect/_vendor/archify/references/delivery-contract.md +120 -0
- graphitect/_vendor/archify/references/viewer-runtime.md +45 -0
- graphitect/_vendor/archify/renderers/architecture/grid.mjs +62 -0
- graphitect/_vendor/archify/renderers/architecture/render-architecture.mjs +1078 -0
- graphitect/_vendor/archify/renderers/dataflow/README.md +104 -0
- graphitect/_vendor/archify/renderers/dataflow/render-dataflow.mjs +483 -0
- graphitect/_vendor/archify/renderers/lifecycle/README.md +115 -0
- graphitect/_vendor/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
- graphitect/_vendor/archify/renderers/sequence/README.md +114 -0
- graphitect/_vendor/archify/renderers/sequence/render-sequence.mjs +464 -0
- graphitect/_vendor/archify/renderers/shared/brand-marks.mjs +563 -0
- graphitect/_vendor/archify/renderers/shared/cli.mjs +218 -0
- graphitect/_vendor/archify/renderers/shared/desktop-readability.mjs +26 -0
- graphitect/_vendor/archify/renderers/shared/diagnostics.mjs +127 -0
- graphitect/_vendor/archify/renderers/shared/engineering-profiles.mjs +157 -0
- graphitect/_vendor/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- graphitect/_vendor/archify/renderers/shared/generated-validators.mjs +13 -0
- graphitect/_vendor/archify/renderers/shared/geometry.mjs +1423 -0
- graphitect/_vendor/archify/renderers/shared/i18n.mjs +595 -0
- graphitect/_vendor/archify/renderers/shared/layout-report.mjs +40 -0
- graphitect/_vendor/archify/renderers/shared/legend.mjs +217 -0
- graphitect/_vendor/archify/renderers/shared/output-path.mjs +340 -0
- graphitect/_vendor/archify/renderers/shared/repository-evidence.mjs +238 -0
- graphitect/_vendor/archify/renderers/shared/repository-location.mjs +58 -0
- graphitect/_vendor/archify/renderers/shared/text-fit.mjs +49 -0
- graphitect/_vendor/archify/renderers/shared/utils.mjs +232 -0
- graphitect/_vendor/archify/renderers/shared/validator.mjs +86 -0
- graphitect/_vendor/archify/renderers/workflow/README.md +223 -0
- graphitect/_vendor/archify/renderers/workflow/render-workflow.mjs +35 -0
- graphitect/_vendor/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- graphitect/_vendor/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- graphitect/_vendor/archify/schemas/README.md +211 -0
- graphitect/_vendor/archify/schemas/architecture.schema.json +178 -0
- graphitect/_vendor/archify/schemas/common.schema.json +115 -0
- graphitect/_vendor/archify/schemas/dataflow.schema.json +243 -0
- graphitect/_vendor/archify/schemas/lifecycle.schema.json +266 -0
- graphitect/_vendor/archify/schemas/sequence.schema.json +223 -0
- graphitect/_vendor/archify/schemas/workflow.schema.json +428 -0
- graphitect/_vendor/archify/scripts/check-render-output.mjs +836 -0
- graphitect/_vendor/archify/scripts/check-update.mjs +1667 -0
- graphitect/_vendor/archify/scripts/generate-brand-marks.mjs +141 -0
- graphitect/_vendor/archify/scripts/generate-validators.mjs +66 -0
- graphitect/_vendor/archify/scripts/render-examples.mjs +26 -0
- graphitect/_vendor/archify/scripts/update-contract.mjs +182 -0
- graphitect/_vendor/archify/skill-release.json +10 -0
- graphitect/cli.py +981 -0
- graphitect/deliver/__init__.py +5 -0
- graphitect/deliver/archify_adapter.py +1877 -0
- graphitect/deliver/archify_ir.py +160 -0
- graphitect/deliver/archify_repair.py +135 -0
- graphitect/deliver/doc_compiler.py +916 -0
- graphitect/ground/__init__.py +5 -0
- graphitect/ground/describe_source.py +27 -0
- graphitect/ground/fullread_source.py +56 -0
- graphitect/ground/graphify_source.py +107 -0
- graphitect/models.py +118 -0
- graphitect/skill/SKILL.md +80 -0
- graphitect/skill/agents/openai.yaml +4 -0
- graphitect/synthesize/__init__.py +5 -0
- graphitect/synthesize/engine.py +281 -0
- graphitect/synthesize/llm_backend.py +331 -0
- graphitect/synthesize/questions.py +139 -0
- graphitect/synthesize/rubric.py +104 -0
- graphitect-0.2.0.dist-info/METADATA +284 -0
- graphitect-0.2.0.dist-info/RECORD +336 -0
- graphitect-0.2.0.dist-info/WHEEL +5 -0
- graphitect-0.2.0.dist-info/entry_points.txt +2 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE +21 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-ARCHIFY-MIT +22 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-APACHE-2.0 +202 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-MIT +21 -0
- graphitect-0.2.0.dist-info/licenses/NOTICE-ARCHIFY-THIRD-PARTY.md +69 -0
- graphitect-0.2.0.dist-info/licenses/NOTICE-GRAPHIFY +8 -0
- graphitect-0.2.0.dist-info/top_level.txt +2 -0
graphify/_minhash.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""MinHash + band-LSH — datasketch-compatible drop-in (no scipy).
|
|
2
|
+
|
|
3
|
+
datasketch.lsh has `from scipy.integrate import quad` at module level.
|
|
4
|
+
scipy's array_api_compat layer then lazily loads numpy.testing, which calls
|
|
5
|
+
platform.machine() at import time to set test-skip decorator constants — and
|
|
6
|
+
that in turn spawns cmd.exe via subprocess, hanging for minutes under EDR
|
|
7
|
+
software in corporate Windows environments.
|
|
8
|
+
|
|
9
|
+
Covers the exact MinHash/MinHashLSH API surface used by dedup.py.
|
|
10
|
+
Hash family (Mersenne-prime permutations) and LSH band structure are
|
|
11
|
+
equivalent to datasketch so dedup quality is unchanged.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
import hashlib
|
|
15
|
+
import struct
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_MP = np.uint64((1 << 61) - 1) # Mersenne prime for the hash family
|
|
21
|
+
_MH = np.uint64(0xFFFF_FFFF) # mask to 32-bit values
|
|
22
|
+
|
|
23
|
+
# One (a, b) coefficient array per num_perm, shared across all instances.
|
|
24
|
+
_MH_COEFFS: dict[int, tuple[np.ndarray, np.ndarray]] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _mh_coeffs(num_perm: int) -> tuple[np.ndarray, np.ndarray]:
|
|
28
|
+
if num_perm not in _MH_COEFFS:
|
|
29
|
+
rng = np.random.RandomState(1)
|
|
30
|
+
a = rng.randint(1, int(_MP), num_perm, dtype=np.uint64)
|
|
31
|
+
b = rng.randint(0, int(_MP), num_perm, dtype=np.uint64)
|
|
32
|
+
_MH_COEFFS[num_perm] = (a, b)
|
|
33
|
+
return _MH_COEFFS[num_perm]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MinHash:
|
|
37
|
+
"""MinHash sketch — same API as datasketch.MinHash for the subset used here."""
|
|
38
|
+
|
|
39
|
+
__slots__ = ("num_perm", "hashvalues", "_a", "_b")
|
|
40
|
+
|
|
41
|
+
def __init__(self, num_perm: int = 128) -> None:
|
|
42
|
+
self.num_perm = num_perm
|
|
43
|
+
self.hashvalues = np.full(num_perm, int(_MH), dtype=np.uint64)
|
|
44
|
+
self._a, self._b = _mh_coeffs(num_perm)
|
|
45
|
+
|
|
46
|
+
def update(self, v: bytes) -> None:
|
|
47
|
+
hv = np.uint64(struct.unpack("<I", hashlib.sha1(v).digest()[:4])[0])
|
|
48
|
+
phv = np.bitwise_and((self._a * hv + self._b) % _MP, _MH)
|
|
49
|
+
self.hashvalues = np.minimum(self.hashvalues, phv)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _lsh_integrate(f, lo: float, hi: float, n: int = 128) -> float:
|
|
53
|
+
"""Numerical integration — replaces scipy.integrate.quad for LSH param search."""
|
|
54
|
+
h = (hi - lo) / n
|
|
55
|
+
return h * sum(f(lo + i * h) for i in range(n))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_LSH_PARAMS_CACHE: dict[tuple[float, int], tuple[int, int]] = {}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _optimal_lsh_params(threshold: float, num_perm: int) -> tuple[int, int]:
|
|
62
|
+
"""Find (bands, rows) that minimise weighted FP+FN error, without scipy."""
|
|
63
|
+
key = (threshold, num_perm)
|
|
64
|
+
if key in _LSH_PARAMS_CACHE:
|
|
65
|
+
return _LSH_PARAMS_CACHE[key]
|
|
66
|
+
best_err, best = float("inf"), (1, 1)
|
|
67
|
+
for b in range(1, num_perm + 1):
|
|
68
|
+
for r in range(1, num_perm // b + 1):
|
|
69
|
+
fp = _lsh_integrate(
|
|
70
|
+
lambda s, _b=float(b), _r=float(r): 1 - (1 - s ** _r) ** _b,
|
|
71
|
+
0.0, threshold,
|
|
72
|
+
)
|
|
73
|
+
fn = _lsh_integrate(
|
|
74
|
+
lambda s, _b=float(b), _r=float(r): 1 - (1 - (1 - s ** _r) ** _b),
|
|
75
|
+
threshold, 1.0,
|
|
76
|
+
)
|
|
77
|
+
err = 0.5 * fp + 0.5 * fn
|
|
78
|
+
if err < best_err:
|
|
79
|
+
best_err, best = err, (b, r)
|
|
80
|
+
_LSH_PARAMS_CACHE[key] = best
|
|
81
|
+
return best
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MinHashLSH:
|
|
85
|
+
"""Band-hashing LSH — same API as datasketch.MinHashLSH for the subset used here."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, threshold: float = 0.5, num_perm: int = 128) -> None:
|
|
88
|
+
self.b, self.r = _optimal_lsh_params(threshold, num_perm)
|
|
89
|
+
self._tables: list[dict[bytes, list[str]]] = [{} for _ in range(self.b)]
|
|
90
|
+
self._keys: set[str] = set()
|
|
91
|
+
|
|
92
|
+
def insert(self, key: str, minhash: MinHash) -> None:
|
|
93
|
+
if key in self._keys:
|
|
94
|
+
raise ValueError(f"Key {key!r} already exists in MinHashLSH")
|
|
95
|
+
self._keys.add(key)
|
|
96
|
+
hv = minhash.hashvalues
|
|
97
|
+
for i, table in enumerate(self._tables):
|
|
98
|
+
band = hv[i * self.r : (i + 1) * self.r].tobytes()
|
|
99
|
+
table.setdefault(band, []).append(key)
|
|
100
|
+
|
|
101
|
+
def query(self, minhash: MinHash) -> list[str]:
|
|
102
|
+
hv = minhash.hashvalues
|
|
103
|
+
candidates: set[str] = set()
|
|
104
|
+
for i, table in enumerate(self._tables):
|
|
105
|
+
band = hv[i * self.r : (i + 1) * self.r].tobytes()
|
|
106
|
+
candidates.update(table.get(band, []))
|
|
107
|
+
return list(candidates)
|
graphify/affected.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
import unicodedata
|
|
8
|
+
|
|
9
|
+
import networkx as nx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEFAULT_AFFECTED_RELATIONS = (
|
|
13
|
+
"calls",
|
|
14
|
+
"indirect_call",
|
|
15
|
+
"references",
|
|
16
|
+
"imports",
|
|
17
|
+
"imports_from",
|
|
18
|
+
# `import('…')` — emitted by the Svelte/Astro/Vue rescue passes and (since
|
|
19
|
+
# #2575) by plain JS/TS too. Omitting it made every dynamic import
|
|
20
|
+
# invisible to blast-radius traversal even where the edge WAS in the
|
|
21
|
+
# graph, and dynamic import is precisely how codebases break require
|
|
22
|
+
# cycles, so the missing edges sat under the most load-bearing modules.
|
|
23
|
+
"dynamic_import",
|
|
24
|
+
"re_exports",
|
|
25
|
+
"inherits",
|
|
26
|
+
"extends",
|
|
27
|
+
"implements",
|
|
28
|
+
"uses",
|
|
29
|
+
"mixes_in",
|
|
30
|
+
"embeds",
|
|
31
|
+
"requires",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class AffectedHit:
|
|
37
|
+
node_id: str
|
|
38
|
+
depth: int
|
|
39
|
+
via_relation: str
|
|
40
|
+
# The traversed edge's location — the actual call/import/reference SITE in
|
|
41
|
+
# this node's file, not the node's own definition line (#BUG1). Defaults keep
|
|
42
|
+
# existing constructors/tests working; None falls back to the node's def line.
|
|
43
|
+
via_file: "str | None" = None
|
|
44
|
+
via_location: "str | None" = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _node_label(graph: nx.Graph, node_id: str) -> str:
|
|
48
|
+
data = graph.nodes[node_id]
|
|
49
|
+
return str(data.get("label") or node_id)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _format_location(data: dict) -> str:
|
|
53
|
+
source_file = data.get("source_file") or "-"
|
|
54
|
+
source_location = data.get("source_location")
|
|
55
|
+
if source_location:
|
|
56
|
+
return f"{source_file}:{source_location}"
|
|
57
|
+
return str(source_file)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _bare_name(label: str) -> str:
|
|
61
|
+
"""Lowercased label with the callable decoration (trailing "()") removed."""
|
|
62
|
+
label = _normalize_label(label)
|
|
63
|
+
return label[:-2] if label.endswith("()") else label
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _normalize_label(label: str) -> str:
|
|
67
|
+
return unicodedata.normalize("NFC", label).casefold()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _as_repo_relative(query: str, root: Path | None = None) -> str:
|
|
71
|
+
"""Repo-relative form of a path query, for matching a stored `source_file`.
|
|
72
|
+
|
|
73
|
+
The graph stores repo-relative paths, so `./src/x.py` and
|
|
74
|
+
`/abs/repo/src/x.py` name the same file as `src/x.py` and yet matched
|
|
75
|
+
nothing. `affected` then printed an empty list and exited 0 — a blast-radius
|
|
76
|
+
tool answering "nothing depends on this" about a file with sixteen
|
|
77
|
+
dependents, and indistinguishable from a genuine zero or a typo.
|
|
78
|
+
|
|
79
|
+
An absolute path is anchored to `root` when given — the repo root derived
|
|
80
|
+
from the graph's own location — so a seed resolves regardless of the caller's
|
|
81
|
+
working directory (#2706: an absolute-path seed previously only matched when
|
|
82
|
+
cwd happened to be the analysed repo root, which no editor or script can
|
|
83
|
+
guarantee). `root` falls back to the current directory to preserve the prior
|
|
84
|
+
behaviour when a caller has no graph location to derive it from.
|
|
85
|
+
|
|
86
|
+
Non-path queries pass through unchanged: `Path("myFunc()").as_posix()` is
|
|
87
|
+
`"myFunc()"`, so label resolution is untouched. An absolute path rooted
|
|
88
|
+
outside `root` is left alone — no basename guessing.
|
|
89
|
+
"""
|
|
90
|
+
path = Path(query)
|
|
91
|
+
if path.is_absolute():
|
|
92
|
+
anchor = root if root is not None else Path.cwd()
|
|
93
|
+
try:
|
|
94
|
+
return path.relative_to(anchor).as_posix()
|
|
95
|
+
except ValueError:
|
|
96
|
+
# Rooted outside the repo: nothing here can make it repo-relative,
|
|
97
|
+
# so leave it alone rather than guess at a basename that would match
|
|
98
|
+
# some unrelated file with the same name.
|
|
99
|
+
return query
|
|
100
|
+
return path.as_posix()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _prefer_file_node(
|
|
104
|
+
graph: nx.Graph,
|
|
105
|
+
node_ids: list[str],
|
|
106
|
+
query: str,
|
|
107
|
+
) -> str | None:
|
|
108
|
+
"""Return the file-level node when a source_file query matches many nodes."""
|
|
109
|
+
query_basename = _normalize_label(Path(query).name)
|
|
110
|
+
exact_file_nodes = [
|
|
111
|
+
node_id
|
|
112
|
+
for node_id in node_ids
|
|
113
|
+
if str(graph.nodes[node_id].get("source_location", "")) == "L1"
|
|
114
|
+
and _normalize_label(str(graph.nodes[node_id].get("label", ""))) == query_basename
|
|
115
|
+
]
|
|
116
|
+
if len(exact_file_nodes) == 1:
|
|
117
|
+
return exact_file_nodes[0]
|
|
118
|
+
|
|
119
|
+
l1_nodes = [
|
|
120
|
+
node_id
|
|
121
|
+
for node_id in node_ids
|
|
122
|
+
if str(graph.nodes[node_id].get("source_location", "")) == "L1"
|
|
123
|
+
]
|
|
124
|
+
if len(l1_nodes) == 1:
|
|
125
|
+
return l1_nodes[0]
|
|
126
|
+
|
|
127
|
+
basename_nodes = [
|
|
128
|
+
node_id
|
|
129
|
+
for node_id in node_ids
|
|
130
|
+
if _normalize_label(str(graph.nodes[node_id].get("label", ""))) == query_basename
|
|
131
|
+
]
|
|
132
|
+
if len(basename_nodes) == 1:
|
|
133
|
+
return basename_nodes[0]
|
|
134
|
+
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def resolve_seed(graph: nx.Graph, query: str, root: Path | None = None) -> str | None:
|
|
139
|
+
# A trailing path separator must not change a source-file match — serve's
|
|
140
|
+
# _find_node tokenizes the path (which drops it), so strip it here for parity
|
|
141
|
+
# (otherwise `affected "src/x.ts/"` returned None while `explain` resolved it).
|
|
142
|
+
query = query.rstrip("/\\") or query
|
|
143
|
+
if query in graph:
|
|
144
|
+
return query
|
|
145
|
+
query_lower = _normalize_label(query)
|
|
146
|
+
exact_label_matches = [
|
|
147
|
+
str(node_id)
|
|
148
|
+
for node_id, data in graph.nodes(data=True)
|
|
149
|
+
if _normalize_label(str(data.get("label", ""))) == query_lower
|
|
150
|
+
]
|
|
151
|
+
if len(exact_label_matches) == 1:
|
|
152
|
+
return exact_label_matches[0]
|
|
153
|
+
# Callable labels are decorated ("name()"), so a bare "name" query falls
|
|
154
|
+
# through exact matching and then ties with any "name*" sibling in the
|
|
155
|
+
# contains pass. Match on the undecorated name before giving up.
|
|
156
|
+
query_bare = _bare_name(query_lower)
|
|
157
|
+
bare_name_matches = [
|
|
158
|
+
str(node_id)
|
|
159
|
+
for node_id, data in graph.nodes(data=True)
|
|
160
|
+
if _bare_name(str(data.get("label", ""))) == query_bare
|
|
161
|
+
]
|
|
162
|
+
if len(bare_name_matches) == 1:
|
|
163
|
+
return bare_name_matches[0]
|
|
164
|
+
# Compare paths in repo-relative form. Only this branch is path-shaped; the
|
|
165
|
+
# label branches above keep the query verbatim.
|
|
166
|
+
query_path = _normalize_label(_as_repo_relative(query, root))
|
|
167
|
+
exact_source_matches = [
|
|
168
|
+
str(node_id)
|
|
169
|
+
for node_id, data in graph.nodes(data=True)
|
|
170
|
+
if _normalize_label(str(data.get("source_file", ""))) in (query_lower, query_path)
|
|
171
|
+
]
|
|
172
|
+
if len(exact_source_matches) == 1:
|
|
173
|
+
return exact_source_matches[0]
|
|
174
|
+
if exact_source_matches:
|
|
175
|
+
preferred_file_node = _prefer_file_node(
|
|
176
|
+
graph, exact_source_matches, _as_repo_relative(query, root)
|
|
177
|
+
)
|
|
178
|
+
if preferred_file_node is not None:
|
|
179
|
+
return preferred_file_node
|
|
180
|
+
contains_matches = [
|
|
181
|
+
str(node_id)
|
|
182
|
+
for node_id, data in graph.nodes(data=True)
|
|
183
|
+
if query_lower in _normalize_label(str(data.get("label", "")))
|
|
184
|
+
]
|
|
185
|
+
if len(contains_matches) == 1:
|
|
186
|
+
return contains_matches[0]
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def affected_nodes(
|
|
191
|
+
graph: nx.Graph,
|
|
192
|
+
seed: str,
|
|
193
|
+
*,
|
|
194
|
+
relations: Iterable[str] = DEFAULT_AFFECTED_RELATIONS,
|
|
195
|
+
depth: int = 2,
|
|
196
|
+
) -> list[AffectedHit]:
|
|
197
|
+
relation_set = set(relations)
|
|
198
|
+
seen = {seed}
|
|
199
|
+
queue: deque[tuple[str, int]] = deque([(seed, 0)])
|
|
200
|
+
hits: list[AffectedHit] = []
|
|
201
|
+
|
|
202
|
+
# #1669: seed the reverse walk with the root's own member nodes (one outward
|
|
203
|
+
# `method`/`contains` hop). A caller can bind to a class's method node rather
|
|
204
|
+
# than the class node itself (e.g. `Service.call` resolves to the `def
|
|
205
|
+
# self.call` node, #1634), so those callers are unreachable from the class
|
|
206
|
+
# otherwise. The member nodes are seeds only (not reported as hits), and
|
|
207
|
+
# `method`/`contains` stay out of the general relation-filtered walk, so this
|
|
208
|
+
# adds no forward noise anywhere else.
|
|
209
|
+
if hasattr(graph, "out_edges"):
|
|
210
|
+
member_edges = graph.out_edges(seed, data=True)
|
|
211
|
+
else:
|
|
212
|
+
member_edges = (
|
|
213
|
+
(s, t, d) for s, t, d in graph.edges(data=True) if s == seed
|
|
214
|
+
)
|
|
215
|
+
for _s, member, data in member_edges:
|
|
216
|
+
if str(data.get("relation", "")) not in ("method", "contains"):
|
|
217
|
+
continue
|
|
218
|
+
member = str(member)
|
|
219
|
+
if member not in seen:
|
|
220
|
+
seen.add(member)
|
|
221
|
+
queue.append((member, 0))
|
|
222
|
+
|
|
223
|
+
while queue:
|
|
224
|
+
current, current_depth = queue.popleft()
|
|
225
|
+
if current_depth >= depth:
|
|
226
|
+
continue
|
|
227
|
+
if hasattr(graph, "in_edges"):
|
|
228
|
+
incoming = graph.in_edges(current, data=True)
|
|
229
|
+
else:
|
|
230
|
+
incoming = (
|
|
231
|
+
(source, target, data)
|
|
232
|
+
for source, target, data in graph.edges(data=True)
|
|
233
|
+
if target == current
|
|
234
|
+
)
|
|
235
|
+
for source, _target, data in incoming:
|
|
236
|
+
relation = str(data.get("relation", ""))
|
|
237
|
+
if relation not in relation_set:
|
|
238
|
+
continue
|
|
239
|
+
source = str(source)
|
|
240
|
+
if source in seen:
|
|
241
|
+
continue
|
|
242
|
+
seen.add(source)
|
|
243
|
+
# Carry the matched edge's location (taken from the SAME edge dict
|
|
244
|
+
# whose relation passed the filter, so relation and location stay
|
|
245
|
+
# consistent) — that is the call/import/reference site in `source`'s
|
|
246
|
+
# own file, which is where the user should click (#BUG1).
|
|
247
|
+
hit = AffectedHit(
|
|
248
|
+
source, current_depth + 1, relation,
|
|
249
|
+
via_file=str(data.get("source_file") or "") or None,
|
|
250
|
+
via_location=str(data.get("source_location") or "") or None,
|
|
251
|
+
)
|
|
252
|
+
hits.append(hit)
|
|
253
|
+
queue.append((source, current_depth + 1))
|
|
254
|
+
|
|
255
|
+
return hits
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def format_affected(
|
|
259
|
+
graph: nx.Graph,
|
|
260
|
+
query: str,
|
|
261
|
+
*,
|
|
262
|
+
relations: Iterable[str] = DEFAULT_AFFECTED_RELATIONS,
|
|
263
|
+
depth: int = 2,
|
|
264
|
+
root: Path | None = None,
|
|
265
|
+
) -> str:
|
|
266
|
+
relation_list = tuple(relations)
|
|
267
|
+
seed = resolve_seed(graph, query, root)
|
|
268
|
+
if seed is None:
|
|
269
|
+
return f"No unique node match for {query}"
|
|
270
|
+
|
|
271
|
+
hits = affected_nodes(graph, seed, relations=relation_list, depth=depth)
|
|
272
|
+
lines = [
|
|
273
|
+
f"Affected nodes for {_node_label(graph, seed)}",
|
|
274
|
+
f"Relations: {', '.join(relation_list)}",
|
|
275
|
+
f"Depth: {depth}",
|
|
276
|
+
]
|
|
277
|
+
if not hits:
|
|
278
|
+
lines.append("No affected nodes found.")
|
|
279
|
+
return "\n".join(lines)
|
|
280
|
+
|
|
281
|
+
for hit in hits:
|
|
282
|
+
data = graph.nodes[hit.node_id]
|
|
283
|
+
if hit.via_location:
|
|
284
|
+
# The relation SITE in this node's file (call/import/reference line),
|
|
285
|
+
# labeled by [via_relation] so it's never mistaken for a def line.
|
|
286
|
+
location = f"{hit.via_file or data.get('source_file') or '-'}:{hit.via_location}"
|
|
287
|
+
else:
|
|
288
|
+
location = _format_location(data) # honest fallback: the node's own def line
|
|
289
|
+
lines.append(
|
|
290
|
+
f"- {_node_label(graph, hit.node_id)} [{hit.via_relation}] {location}"
|
|
291
|
+
)
|
|
292
|
+
return "\n".join(lines)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def load_graph(path: Path) -> nx.Graph:
|
|
296
|
+
import json
|
|
297
|
+
from networkx.readwrite import json_graph
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
301
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
302
|
+
raise RuntimeError(
|
|
303
|
+
f"Cannot read graph file {path}: {exc}. "
|
|
304
|
+
"Re-run 'graphify extract' to regenerate it."
|
|
305
|
+
) from exc
|
|
306
|
+
# Force directed so stored caller→callee direction survives the round-trip;
|
|
307
|
+
# mirrors serve.py and __main__.py (#1174).
|
|
308
|
+
raw = {**raw, "directed": True}
|
|
309
|
+
# Normalize the edge key: graphify's `extract` output uses "edges" while
|
|
310
|
+
# networkx's node_link_data default is "links". Without this, an edges-keyed
|
|
311
|
+
# graph.json raises an uncaught KeyError: 'links' here — every other loader
|
|
312
|
+
# (__main__.py) already normalizes this (#738; same class as #1198).
|
|
313
|
+
if "links" not in raw and "edges" in raw:
|
|
314
|
+
raw = dict(raw, links=raw["edges"])
|
|
315
|
+
try:
|
|
316
|
+
return json_graph.node_link_graph(raw, edges="links")
|
|
317
|
+
except TypeError:
|
|
318
|
+
return json_graph.node_link_graph(raw)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
## graphify
|
|
2
|
+
|
|
3
|
+
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
|
|
4
|
+
|
|
5
|
+
When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else.
|
|
6
|
+
|
|
7
|
+
Rules:
|
|
8
|
+
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
|
|
9
|
+
- 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.
|
|
10
|
+
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
|
|
11
|
+
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
|
|
12
|
+
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: always_on
|
|
3
|
+
description: Consult the graphify knowledge graph at graphify-out/ for codebase and architecture questions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## graphify
|
|
7
|
+
|
|
8
|
+
This project has a graphify knowledge graph at graphify-out/.
|
|
9
|
+
|
|
10
|
+
Rules:
|
|
11
|
+
- For codebase or architecture questions, when `graphify-out/graph.json` exists, first run `graphify query "<question>"` (CLI) or `query_graph` (MCP). Use `graphify path "<A>" "<B>"` / `shortest_path` for relationships and `graphify explain "<concept>"` / `get_node` for focused concepts. These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output.
|
|
12
|
+
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
|
|
13
|
+
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context
|
|
14
|
+
- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## graphify
|
|
2
|
+
|
|
3
|
+
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
|
|
4
|
+
|
|
5
|
+
Rules:
|
|
6
|
+
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
|
|
7
|
+
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
|
|
8
|
+
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
|
|
9
|
+
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## graphify
|
|
2
|
+
|
|
3
|
+
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
|
|
4
|
+
|
|
5
|
+
Rules:
|
|
6
|
+
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
|
|
7
|
+
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
|
|
8
|
+
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
|
|
9
|
+
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
inclusion: always
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
graphify: A knowledge graph of this project lives in `graphify-out/`. For codebase, architecture, or dependency questions, when `graphify-out/graph.json` exists, first run `graphify query "<question>"` (or `graphify path "<A>" "<B>"` / `graphify explain "<concept>"`). These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output. Read `GRAPH_REPORT.md` only for broad architecture review or when those commands do not surface enough context.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## graphify
|
|
2
|
+
|
|
3
|
+
For any question about this repo's architecture, structure, components, or how to add/modify/find
|
|
4
|
+
code, your first action should be `graphify query "<question>"` when `graphify-out/graph.json`
|
|
5
|
+
exists. Use `graphify path "<A>" "<B>"` for relationship questions and `graphify explain "<concept>"`
|
|
6
|
+
for focused-concept questions. These return a scoped subgraph, usually much smaller than the full
|
|
7
|
+
report or raw grep output.
|
|
8
|
+
|
|
9
|
+
Triggers: "how do I…", "where is…", "what does … do", "add/modify a <component>",
|
|
10
|
+
"explain the architecture", or anything that depends on how files or classes relate.
|
|
11
|
+
|
|
12
|
+
If `graphify-out/wiki/index.md` exists, use it for broad navigation. Read `graphify-out/GRAPH_REPORT.md`
|
|
13
|
+
only for broad architecture review or when query/path/explain do not surface enough context. Only read
|
|
14
|
+
source files when (a) modifying/debugging specific code, (b) the graph lacks the needed detail, or
|
|
15
|
+
(c) the graph is missing or stale.
|
|
16
|
+
|
|
17
|
+
Type `/graphify` in Copilot Chat to build or update the graph.
|