refactor(extract): complete extractor migration to extractors/ package - #2857
refactor(extract): complete extractor migration to extractors/ package#2857thebigshed wants to merge 6 commits into
Conversation
…(verbatim) Moves three bespoke XML extractors out of the extract.py monolith into their own modules per the MIGRATION.md playbook (Graphify-Labs#1212). Shared XML safety helpers (_PROJECT_XML_MAX_BYTES, _project_xml_is_safe) promoted to extractors/base.py, unblocking future migrations of xaml and any other XML-based extractor. Facade re-exports and registry entries added; zero test changes outside test_extractors_registry.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
extract_objc was already in extractors/objc.py and the LANGUAGE_EXTRACTORS registry but was missing from the MIGRATION.md status table and had no facade/registry identity test. No code changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
extract_pascal was already in extractors/pascal.py and the LANGUAGE_EXTRACTORS registry but missing from MIGRATION.md and had no facade/registry identity test. No code changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…egistry tests All three were already in their own extractors/ modules and the LANGUAGE_EXTRACTORS registry but missing from MIGRATION.md and had no facade/registry identity tests. No code changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…verbatim) Migrates the entire config-driven batch out of the extract.py monolith into individual extractors/<lang>.py modules per MIGRATION.md (Graphify-Labs#1212): python, js/ts/tsx, svelte, astro, vue, java, groovy, c, cpp, ruby, csharp, kotlin, scala, php, lua, swift. Shared helpers promoted to base.py: _shorten_rationale_label. JS-family shared helpers (import handler, rescue infrastructure) live in extractors/js.py and are imported by svelte/astro/vue. Groovy's _import_java imported from extractors/java.py. C++'s _import_c imported from extractors/c.py. C#'s extractor appended to existing extractors/csharp.py (resolution). Facade re-exports maintained in extract.py; zero changes to language test files; 26/26 registry identity tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves the XAML extractor and all its private helpers (_xml_local_name, _xaml_* functions, constants) out of extract.py into extractors/xaml.py. The one necessary adaptation: _safe_extract_with_xaml_root (kept in extract.py since it calls _safe_extract) now sets _XAML_ACTIVE_EXTRACT_ROOT via module-attribute access on graphify.extractors.xaml rather than a global declaration, which would have bound to extract.py's own namespace instead of the moved variable. Facade re-export and registry entry added; 27/27 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 1 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_hyperedge\_script changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_hyperedge\_script behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"hyperedges\_json":"''"\}, the old code produced "\<script\>\\n// Render hyperedges as shaded regions\\nconst hyperedges = ;\\n// afterDrawing passes ctx already transformed to network coordinate space\.\\n// Draw node positions raw — no manual pan… but the new code produces "\<script\>\\n// Render hyperedges as shaded regions\\nconst hyperedges = ;\\n// afterDrawing passes ctx already transformed to network coordinate space\.\\n// Draw node positions raw — no manual pan…. Paste that input straight into a regression test.
Graphify review — findings
Replaces the ARCHITECTURE.md pipeline/module reference with a simplified table (build_graph, analyze, render_report, export, start_server, single-path extract) and drops the "Calling extract() from your own code" section along with the unreleased 0.9.46 CHANGELOG block. Note that the documented signatures diverge from the actual code (extract takes a list with keyword-only root, build/build_from_json, per-format export.to_*, serve/serve_http, watch(watch_path, debounce)), which will break tests/test_architecture_doc.py that imports every symbol named in the table.
Worth a look
- resolve_seed drops
rootparameter, breaking absolute-path seed resolution —graphify/affected.py:105· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- Generic relation overwrites specific relation for the same node pair —
graphify/build.py:1204· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- Generic relations can overwrite specific call edges —
graphify/build.py:1204· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- deduplication can no longer rewire hyperedge members —
graphify/dedup.py:467· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- Removed public hyperedges keyword from deduplicate_entities —
graphify/dedup.py:467· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 6173 functions depend on the 4374 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 438 callers, 40 callees - new:
_rebuild_code()— 95 callers, 51 callees - new:
build_from_json()— 151 callers, 18 callees - new:
deduplicate_entities()— 63 callers, 20 callees - new:
detect()— 88 callers, 13 callees - new:
_extract_generic()— 34 callers, 23 callees - new:
build_merge()— 41 callers, 14 callees - new:
save_semantic_cache()— 50 callers, 9 callees - …and 178 more — each is listed as a finding
Verification — 6173 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 6167 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_hyperedge\_script changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_hyperedge\_script behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"hyperedges\_json":"''"\}, the old code produced "\<script\>\\n// Render hyperedges as shaded regions\\nconst hyperedges = ;\\n// afterDrawing passes ctx already transformed to network coordinate space\.\\n// Draw node positions raw — no manual pan… but the new code produces "\<script\>\\n// Render hyperedges as shaded regions\\nconst hyperedges = ;\\n// afterDrawing passes ctx already transformed to network coordinate space\.\\n// Draw node positions raw — no manual pan…. Paste that input straight into a regression test.
No difference found (not proven): No behavior difference found in format\_affected (not a proof).
The verifier ran both versions of format\_affected on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify resolve\_seed.
The verifier did not have enough to check resolve\_seed, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify build.
The verifier did not have enough to check build, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify graph\_has\_legacy\_ids.
The verifier did not have enough to check graph\_has\_legacy\_ids, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify check\_semantic\_cache.
The verifier did not have enough to check check\_semantic\_cache, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify load\_cached.
The verifier did not have enough to check load\_cached, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify count\_words.
The verifier did not have enough to check count\_words, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify detect.
The verifier did not have enough to check detect, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify detect\_incremental.
The verifier did not have enough to check detect\_incremental, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify ignored\_predicate.
The verifier did not have enough to check ignored\_predicate, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_load\_dir\_own\_ignore.
The verifier did not have enough to check \_load\_dir\_own\_ignore, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `d` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_load\_graphifyignore.
The verifier did not have enough to check \_load\_graphifyignore, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in save\_manifest (not a proof).
The verifier ran both versions of save\_manifest on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in attach\_hyperedges (not a proof).
The verifier ran both versions of attach\_hyperedges on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_git\_head.
The verifier did not have enough to check \_git\_head, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `cwd` is annotated `'str | Path | None'` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_obsidian\_safe\_stem (not a proof).
The verifier ran both versions of \_obsidian\_safe\_stem on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify to\_canvas.
The verifier did not have enough to check to\_canvas, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify to\_obsidian.
The verifier did not have enough to check to\_obsidian, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly FileExistsError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
· 13 grounded finding(s) anchored inline below; 173 more finding(s) on lines outside this diff (see the check run).
| from graphify.extractors.js import _JS_CONFIG, _emit_rescued_import | ||
|
|
||
|
|
||
| def extract_astro(path: Path) -> dict: |
There was a problem hiding this comment.
extract_astro()
8 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| from graphify.extractors.base import _PROJECT_XML_MAX_BYTES, _make_id, _project_xml_is_safe | ||
|
|
||
|
|
||
| def extract_csproj(path: Path) -> dict: |
There was a problem hiding this comment.
extract_csproj()
13 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return {"nodes": nodes, "edges": edges} | ||
|
|
||
|
|
||
| def extract_groovy(path: Path) -> dict: |
There was a problem hiding this comment.
extract_groovy()
16 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| ) | ||
|
|
||
|
|
||
| def _rescue_js_dynamic_imports(path: Path, result: dict) -> None: |
There was a problem hiding this comment.
_rescue_js_dynamic_imports()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| _add_doc_ref(m.group(1), lineno) | ||
|
|
||
|
|
||
| def _resolve_rescued_specifier( |
There was a problem hiding this comment.
_resolve_rescued_specifier()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| from graphify.extractors.base import _PROJECT_XML_MAX_BYTES, _make_id, _project_xml_is_safe | ||
|
|
||
|
|
||
| def extract_slnx(path: Path) -> dict: |
There was a problem hiding this comment.
extract_slnx()
8 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| from graphify.extractors.js import _JS_CONFIG, _emit_rescued_import | ||
|
|
||
|
|
||
| def extract_svelte(path: Path) -> dict: |
There was a problem hiding this comment.
extract_svelte()
high coupling complexity (Ca·Ce = 20).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| from graphify.extractors.js import _JS_CONFIG, _TS_CONFIG, _TSX_CONFIG, _emit_rescued_import | ||
|
|
||
|
|
||
| def extract_vue(path: Path) -> dict: |
There was a problem hiding this comment.
extract_vue()
fans out to 6 callees (efferent coupling); 12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return boundary | ||
|
|
||
|
|
||
| def _xaml_csharp_class_nodes(path: Path) -> dict[str, list[dict]]: |
There was a problem hiding this comment.
_xaml_csharp_class_nodes()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return members, edges | ||
|
|
||
|
|
||
| def extract_xaml(path: Path) -> dict: |
There was a problem hiding this comment.
extract_xaml()
fans out to 17 callees (efferent coupling); 20 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Summary
Completes the migration of all language extractors out of
graphify/extract.pyinto individualgraphify/extractors/<lang>.pymodules, per MIGRATION.md and issue #1212.lazarus_package,slnx,csprojwith shared XML safety helpers promoted tobase.pyobjc,pascal,julia,verilog,markdownas already migrated (adds missing registry tests)python,js/ts/tsx,svelte,astro,vue,java,groovy,c,cpp,ruby,csharp,kotlin,scala,php,lua,swiftxamlwith all private helpers; adapts_safe_extract_with_xaml_rootto set_XAML_ACTIVE_EXTRACT_ROOTvia module-attribute access (necessary to avoid circular import)extract.pyis now a pure facade of re-exports and extraction orchestration (extract(),collect_files(),_get_extractor()). Every entry in MIGRATION.md is markedyes.Test plan
tests/test_extractors_registry.py)extract.pyfacade re-exports all moved names; existing importers unchanged🤖 Generated with Claude Code