Skip to content

feat(extract): add Perl extractor — packages and subs (.pl/.pm, tree-sitter-perl) - #3013

Open
ad-astra-bot wants to merge 3 commits into
Graphify-Labs:v8from
ad-astra-bot:feature/perl-kern
Open

feat(extract): add Perl extractor — packages and subs (.pl/.pm, tree-sitter-perl)#3013
ad-astra-bot wants to merge 3 commits into
Graphify-Labs:v8from
ad-astra-bot:feature/perl-kern

Conversation

@ad-astra-bot

Copy link
Copy Markdown

References #419 ("Add Perl support via tree-sitter", open since 2026-04-17). First slice of the decomposition sketched in #1788 — that PR stays open as the umbrella until all slices land.

This slice: package and sub extraction only — deliberately narrow so it is reviewable without CI (the workflow runs on outside PRs stand at action_required, so a reviewer sees this diff raw).

  • .pl/.pm in CODE_EXTENSIONS; extensionless #!/usr/bin/perl scripts now dispatch through _SHEBANG_DISPATCH (previously detected-but-dropped)
  • Package declarations in all three forms: statement (package Foo;), block (package Foo { ... } with scope restore — statements after the block are not mis-attributed), and mid-file switches
  • Sub definitions including qualified sub Pkg::sub declarations; Perl's implicit main package is materialized lazily so package-less code gets an honest scope without emitting empty nodes for files that don't need one
  • Package names are validated ASCII identifiers before they become labels — the name child is arbitrary source text, and a crafted string would otherwise flow into node labels and exports
  • Iterative tree walks with a per-sibling traversal budget: pathologically deep or wide files keep a partial graph instead of dying to RecursionError

Deliberately out of scope here, following as separate slices: use/require imports with in-corpus re-pointing, @ISA/use parent/use base inheritance, and cross-file call resolution (#1788 has those parts; each will come as its own reviewable change).

Verification:

  • Full test suite: 4899 passed, 11 skipped, 2 failed — both failures reproduce identically on a pristine v8 worktree (test_graphifyignore_hermetic_without_vcs, test_no_git_repo_raises; both assume a directory without VCS)
  • Real-corpus consistency check (Foswiki core/lib, 378 Perl files, 0 parse errors): the reduced extractor's contains edge set is identical to the full extractor's from feat(extract): add Perl support (.pl/.pm, tree-sitter-perl) #1788 (2385 edges), i.e. the package/sub structure is unchanged by the slicing
  • One existing test updated intentionally: test_extensionless_without_usable_shebang_stays_unsupported fixed the old unmapped-perl behavior; its replacement asserts the new dispatch plus keeps the fish case as the still-unmapped guard

tree-sitter-perl >=1.2.0,<2.0 added to core dependencies (not an extra) since .pl/.pm join the default code extensions.

@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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Formal verification. 2 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a Perl extractor (graphify/extractors/perl.py) for .pl/.pm files covering package declarations (statement, block, and mid-file-switch forms plus implicit main) and sub definitions including qualified sub Pkg::sub, with budget-bounded iterative tree walks. Wires it into detect.CODE_EXTENSIONS, extract._DISPATCH, _LANG_FAMILY_BY_EXT, the perl shebang route, and the LANGUAGE_EXTRACTORS/extractors.__init__ registry. Bumps the README grammar count to 38 and adds a CHANGELOG entry for 0.9.49.

No blocking issues surfaced. 4 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2787 functions depend on the 1454 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 474 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_manifest() — 39 callers, 11 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: extract_js() — 80 callers, 3 callees
  • …and 39 more — each is listed as a finding

Verification — 2787 functions in the blast radius were not formally verified this run (proofs are advisory here).

Health delta baseline: last indexed commit b2cd362, 31 commit(s) behind this PR's base.

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: 2592 function(s) in the blast radius were not formally verified this run

Formal verification

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 save\_semantic\_cache.

The verifier did not have enough to check save\_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 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 \_stamped\_manifest\_files.

The verifier did not have enough to check \_stamped\_manifest\_files, 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 to\_obsidian (not a proof).

The verifier ran both versions of to\_obsidian 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 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

Could not verify: Could not verify \_rewire\_unique\_stub\_nodes.

The verifier did not have enough to check \_rewire\_unique\_stub\_nodes, 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: the input domain has 81 values but only 9 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

Could not verify: Could not verify extract\_bash.

The verifier did not have enough to check extract\_bash, 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 extract\_commonlisp.

The verifier did not have enough to check extract\_commonlisp, 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 \_extract\_generic.

The verifier did not have enough to check \_extract\_generic, 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 extract\_julia.

The verifier did not have enough to check extract\_julia, 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 extract\_powershell.

The verifier did not have enough to check extract\_powershell, 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 \_load\_tsconfig\_aliases.

The verifier did not have enough to check \_load\_tsconfig\_aliases, 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 `start_dir` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_load\_tsconfig\_base\_url.

The verifier did not have enough to check \_load\_tsconfig\_base\_url, 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 `start_dir` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_merge\_decl\_def\_classes.

The verifier did not have enough to check \_merge\_decl\_def\_classes, 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: the input domain has 81 values but only 9 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

Could not verify: Could not verify extract\_sql.

The verifier did not have enough to check extract\_sql, 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 extract\_zig.

The verifier did not have enough to check extract\_zig, 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 bisect\_slice.

The verifier did not have enough to check bisect\_slice, 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 `fs` is annotated `FileSlice` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in expand\_oversized\_files (not a proof).

The verifier ran both versions of expand\_oversized\_files 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 is\_splittable\_text.

The verifier did not have enough to check is\_splittable\_text, 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 read\_slice\_text.

The verifier did not have enough to check read\_slice\_text, 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 `fs` is annotated `FileSlice` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_estimate\_file\_tokens.

The verifier did not have enough to check \_estimate\_file\_tokens, 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 `unit` is annotated `'Path | FileSlice'` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, 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

· 2 grounded finding(s) anchored inline below; 45 more finding(s) on lines outside this diff (see the check run).

)


def extract_perl(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_perl()

10 callers depend on it (afferent coupling).

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

return False
return True

def walk_statements(root_node) -> 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 regressionwalk_statements()

fans out to 7 callees (efferent coupling).

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

@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 1 advisory finding(s) below merit a look before merge.

Formal verification. 2 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a Perl extractor (graphify/extractors/perl.py) covering package declarations (statement, block, and mid-file forms plus implicit main) and sub definitions, wired into _DISPATCH, _SHEBANG_DISPATCH, _LANG_FAMILY_BY_EXT, and the LANGUAGE_EXTRACTORS registry. Registers .pl/.pm in CODE_EXTENSIONS, bumps the README grammar count to 38, and adds a 0.9.49 changelog entry. Call resolution, imports, and inheritance are explicitly deferred to later slices.

Worth a look

  • Extractor package import can fail because Perl module is not addedgraphify/extractors/__init__.py:28 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2795 functions depend on the 1462 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 474 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_manifest() — 39 callers, 11 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: extract_js() — 80 callers, 3 callees
  • …and 39 more — each is listed as a finding

Verification — 2795 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: 2600 function(s) in the blast radius were not formally verified this run

Formal verification

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 save\_semantic\_cache.

The verifier did not have enough to check save\_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 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 \_stamped\_manifest\_files.

The verifier did not have enough to check \_stamped\_manifest\_files, 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 to\_obsidian (not a proof).

The verifier ran both versions of to\_obsidian 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 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

Could not verify: Could not verify \_rewire\_unique\_stub\_nodes.

The verifier did not have enough to check \_rewire\_unique\_stub\_nodes, 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: the input domain has 81 values but only 9 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

Could not verify: Could not verify extract\_bash.

The verifier did not have enough to check extract\_bash, 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 extract\_commonlisp.

The verifier did not have enough to check extract\_commonlisp, 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 \_extract\_generic.

The verifier did not have enough to check \_extract\_generic, 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 extract\_julia.

The verifier did not have enough to check extract\_julia, 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 extract\_powershell.

The verifier did not have enough to check extract\_powershell, 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 \_load\_tsconfig\_aliases.

The verifier did not have enough to check \_load\_tsconfig\_aliases, 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 `start_dir` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_load\_tsconfig\_base\_url.

The verifier did not have enough to check \_load\_tsconfig\_base\_url, 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 `start_dir` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_merge\_decl\_def\_classes.

The verifier did not have enough to check \_merge\_decl\_def\_classes, 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: the input domain has 81 values but only 9 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

Could not verify: Could not verify extract\_sql.

The verifier did not have enough to check extract\_sql, 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 extract\_zig.

The verifier did not have enough to check extract\_zig, 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 bisect\_slice.

The verifier did not have enough to check bisect\_slice, 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 `fs` is annotated `FileSlice` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in expand\_oversized\_files (not a proof).

The verifier ran both versions of expand\_oversized\_files 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 is\_splittable\_text.

The verifier did not have enough to check is\_splittable\_text, 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 read\_slice\_text.

The verifier did not have enough to check read\_slice\_text, 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 `fs` is annotated `FileSlice` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_estimate\_file\_tokens.

The verifier did not have enough to check \_estimate\_file\_tokens, 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 `unit` is annotated `'Path | FileSlice'` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, 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

· 2 grounded finding(s) anchored inline below; 45 more finding(s) on lines outside this diff (see the check run).

)


def extract_perl(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_perl()

14 callers depend on it (afferent coupling).

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

return False
return True

def walk_statements(root_node) -> 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 regressionwalk_statements()

fans out to 7 callees (efferent coupling).

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

@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 2 advisory finding(s) below merit a look before merge.


Graphify review — findings

Adds a Perl extractor (extract_perl) built on tree-sitter-perl that recognizes package declarations in statement, block (package Foo { ... } with correct scope restore), and mid-file switch forms, materializes Perl's implicit main package lazily for package-less code, and emits sub definitions including qualified sub Pkg::sub names; imports, inheritance, and call resolution are deferred to later slices. Registers .pl/.pm across the dispatch, language-family, extractor, and code-extension tables, and routes extensionless #!/usr/bin/perl scripts through the shebang map. Guards against runaway files with iterative, node-budget-bounded tree walks that keep partial output on exhaustion, and discards malformed or non-ASCII package names rather than labeling them.

Worth a look

  • block-form package name restore uses stale scope after descentgraphify/extractors/perl.py:160 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Perl walker skips declarations inside control-flow blocksgraphify/extractors/perl.py:184 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2995 functions depend on the 1505 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 498 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_manifest() — 40 callers, 11 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: extract_js() — 83 callers, 3 callees
  • …and 42 more — each is listed as a finding

Verification — 2995 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: 2768 function(s) in the blast radius were not formally verified this run

· 2 grounded finding(s) anchored inline below; 48 more finding(s) on lines outside this diff (see the check run).

)


def extract_perl(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_perl()

14 callers depend on it (afferent coupling).

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

return False
return True

def walk_statements(root_node) -> 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 regressionwalk_statements()

fans out to 7 callees (efferent coupling).

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

Perl .pl/.pm files (and extensionless #!/usr/bin/perl shebang scripts, now
mapped in _SHEBANG_DISPATCH) extract package declarations in all three forms —
statement, block `package Foo { ... }` with scope restore for statements that
follow the block, and mid-file switches — plus sub definitions including
qualified `sub Pkg::sub` declarations and Perl's implicit `main` package,
materialized lazily so a file with no package-less subs emits no empty node.

Package names are validated ASCII identifiers before they become labels: the
tree-sitter name child is arbitrary source text, and a crafted or malformed
name would otherwise flow raw into node labels and on into exports. The tree
walks are iterative with a per-sibling traversal budget, so pathologically
deep or wide files keep a partial graph instead of RecursionError-dropping.

Imports (`use`/`require`), inheritance (\@ISA/use parent/use base), and call
resolution are deliberately out of scope here; they follow as separate slices
so each lands reviewable. Closes the extensionless-perl gap in the shebang
dispatch (Graphify-Labs#419 mentions the demand).
37 -> 38: .pl/.pm join the code grammar table; entry under 0.9.49
(unreleased).
…onicalize root-qualified names

Two gaps found by review:

A bare `{ ... }` block or a phaser (`BEGIN`/`CHECK`/`INIT`/`END`/`UNITCHECK`)
containing `package`/`sub` declarations was never traversed — the walker only
descended into blocks attached directly to a `package_statement`, so valid
declarations inside ordinary scopes were silently missing. Both forms now
push a walker frame at the enclosing scope; the existing restore-on-exit keeps
statements after the block attributed correctly.

Root-qualified names (::Name == main::Name) produced an empty-label package
node via rpartition on `sub ::foo {}`, and `package ::Outer;` failed label
validation outright. A leading :: is now stripped before validation and
container selection, so both spellings key to the same canonical package node.

Real-corpus effect (Foswiki core/lib): contains edges 2385 -> 2402 — the
missing declarations were predominantly inside BEGIN blocks.

@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 3 advisory finding(s) below merit a look before merge.


Graphify review — findings

Adds Perl .pl/.pm extraction backed by tree-sitter-perl in a new extract_perl, wired into the dispatch tables, _LANG_FAMILY_BY_EXT, CODE_EXTENSIONS, and the shebang map so extensionless #!/usr/bin/perl scripts also route through. It materializes package declarations in statement, block-scoped, and mid-file-switch forms plus a lazily-created implicit main, and sub definitions including qualified sub Pkg::sub, while validating package names against an ASCII-only pattern so malformed labels are dropped instead of emitted. The tree walks are iterative and capped at _MAX_PERL_TRAVERSAL_NODES, leaving pathologically large files partial rather than crashing; imports, inheritance, and call resolution are deferred to later slices.

Worth a look

  • Restored scope after descent uses stale captured values, mis-attributing subs after nested package blockgraphify/extractors/perl.py:191 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Qualified sub name validation missing before use as labelgraphify/extractors/perl.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Qualified sub name validation missing; malformed package qualifier becomes a node labelgraphify/extractors/perl.py:200 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 3013 functions depend on the 1511 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 500 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_manifest() — 40 callers, 11 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: extract_js() — 85 callers, 3 callees
  • …and 42 more — each is listed as a finding

Verification — 3013 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: 2780 function(s) in the blast radius were not formally verified this run

· 2 grounded finding(s) anchored inline below; 48 more finding(s) on lines outside this diff (see the check run).

)


def extract_perl(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_perl()

14 callers depend on it (afferent coupling).

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

return False
return True

def walk_statements(root_node) -> 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 regressionwalk_statements()

fans out to 7 callees (efferent 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