Skip to content

refactor(extract): complete extractor migration to extractors/ package - #2857

Open
thebigshed wants to merge 6 commits into
Graphify-Labs:v8from
thebigshed:extractor-migration
Open

refactor(extract): complete extractor migration to extractors/ package#2857
thebigshed wants to merge 6 commits into
Graphify-Labs:v8from
thebigshed:extractor-migration

Conversation

@thebigshed

Copy link
Copy Markdown

Summary

Completes the migration of all language extractors out of graphify/extract.py into individual graphify/extractors/<lang>.py modules, per MIGRATION.md and issue #1212.

  • Moves lazarus_package, slnx, csproj with shared XML safety helpers promoted to base.py
  • Records objc, pascal, julia, verilog, markdown as already migrated (adds missing registry tests)
  • Migrates the full config-driven batch: python, js/ts/tsx, svelte, astro, vue, java, groovy, c, cpp, ruby, csharp, kotlin, scala, php, lua, swift
  • Migrates xaml with all private helpers; adapts _safe_extract_with_xaml_root to set _XAML_ACTIVE_EXTRACT_ROOT via module-attribute access (necessary to avoid circular import)

extract.py is now a pure facade of re-exports and extraction orchestration (extract(), collect_files(), _get_extractor()). Every entry in MIGRATION.md is marked yes.

Test plan

  • 27/27 registry identity tests pass (tests/test_extractors_registry.py)
  • Zero changes to language-specific test files — untouched tests passing is the proof of behavior preservation per MIGRATION.md
  • extract.py facade re-exports all moved names; existing importers unchanged

🤖 Generated with Claude Code

thebigshed and others added 6 commits August 18, 2026 18:52
…(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>

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 root parameter, breaking absolute-path seed resolutiongraphify/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 pairgraphify/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 edgesgraphify/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 membersgraphify/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_entitiesgraphify/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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_groovy()

16 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/extractors/js.py
)


def _rescue_js_dynamic_imports(path: Path, result: dict) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_rescue_js_dynamic_imports()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/extractors/js.py
_add_doc_ref(m.group(1), lineno)


def _resolve_rescued_specifier(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_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]]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_xaml()

fans out to 17 callees (efferent coupling); 20 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant