fix(js): keep functions published on window/globalThis, and mask .astro before parsing - #3338
fix(js): keep functions published on window/globalThis, and mask .astro before parsing#3338MK023 wants to merge 1624 commits into
Conversation
…aphify-Labs#2468 follow-up) The Graphify-Labs#2475 fix recomputed the match path against the explicit scan root but skipped the _nfc() normalization rel_anchor gets, so an NFD-named path could miss a parent ignore pattern under the Graphify-Labs#2544 NFC-matching regime. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes: Graphify-Labs#2517 (JS/TS catch-binding indirect_call), Graphify-Labs#2434 (Cargo.toml manifest), Graphify-Labs#2468 (scan-root vs parent .gitignore + NFC follow-up), Graphify-Labs#2482 (API rationale prompt). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…abs#2635/Graphify-Labs#2634 to the 0.9.41 section Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…abs#2661/Graphify-Labs#2457/Graphify-Labs#2674 to the 0.9.41 section Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he shell cwd (Graphify-Labs#2534 family)
Correctness (Graphify-Labs#2685 loop-binding shadow, Graphify-Labs#2699 provenance, Graphify-Labs#2707 affected seed, Graphify-Labs#2688 py subpackage imports, Graphify-Labs#2602 sql grammar, Graphify-Labs#2683 cache integrity, Graphify-Labs#2682 report basename) + Windows portability/docs batch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ring Update the deepseek-v4-flash per-1M-token pricing (same units/ordering as neighboring models) and fix the build_merge docstring, which claimed it saved the graph back to disk; build_merge only reads the existing graph and returns the merged result — callers persist.
…(Win11 focus-steal) Add a comment warning against reverting the Windows detached spawn to DETACHED_PROCESS: DETACHED_PROCESS lets the child allocate its own console, which Windows 11 + Windows Terminal render as a visible focus-stealing window, so CREATE_NO_WINDOW is required.
…aphify-Labs#3190) A code-only watch/update rebuild never re-creates a semantic-backed document node, so an authored [[wikilink]] to such a doc resolved to a missing canonical id and reconcile silently dropped the edge. Re-parse each markdown source and repoint authored references edges onto a unique representative node for the target file (canonical id, else unique page node, else unique document node; ambiguous -> keep the existing edge), preserving links to the semantic tier across a code-only rebuild without resurrecting a deleted target.
… target (Graphify-Labs#3190) The reconcile relies on the extractor only stamping target_file when the link target exists on disk. Add a negative test: link text intact but the target file deleted must leave no reference edge and no edge pointing at the deleted document's node.
…help/pricing batch
Ships three batches: php new Foo() calls (Graphify-Labs#3115), inherited-field receivers (Graphify-Labs#3151), objc field-table remap (Graphify-Labs#3150), ts type-only-import cycles (Graphify-Labs#3123) and import-type args (Graphify-Labs#3185); razor @Inject cross-file (partial Graphify-Labs#3187), MCP prs isError + get_node resolver parity, project-scoped bare hook command (Graphify-Labs#3129); and T-SQL routine recovery (Graphify-Labs#3164), watch markdown->semantic reconcile (Graphify-Labs#3190), --help completeness, and a deepseek pricing fix. The Graphify-Labs#3155 provenance backfill was reverted (regressed the orphan prune). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-Labs#3127) A god-node relation group capped at 20 targets showed no indication it was truncated; append a '…and N more' line (exact count, degree-sorted so the lowest-degree are dropped) only when the group actually exceeds the cap.
…Graphify-Labs#3148) The GRAPH_REPORT headline figures disagreed: shown-community count included empty communities the render loop skips, and the knowledge-gaps thin count used a hardcoded 3 beside a label printing min_community_size. Drive every figure from one _real_count predicate so the counts reconcile with what is actually rendered.
…d few (Graphify-Labs#3183) Only a handful of control tokens were defanged, so an attacker-chosen token outside the set (e.g. Llama-3 <|eot_id|>) could still reach the prompt. Generalize the neutraliser to match the control-token form (<|...|> and [/?INST|SYSTEM]) at the single _wrap_untrusted chokepoint; the bounded charset leaves legitimate content (|| , markdown tables, generics) untouched.
…prose (Graphify-Labs#3121) The hook-guard matched a search command appearing anywhere in the intercepted Bash string (a comment, a quoted -m message, an echo), firing false nudges. Match only an executed command position (start-of-segment after a shell operator, wrapper-aware), so prose lookalikes stay quiet while genuine pipes/&&/;/$() invocations still fire.
…L.md (Graphify-Labs#3144) When the installed SKILL.md has diverged (user edit or old version), back it up beside the install target before the atomic overwrite and print an actionable warning naming the stale path and the exact reinstall command; a clean up-to-date reinstall backs up nothing (no churn), and only the installed copy is touched, never the generated sources.
…bs#3190) Follow-up to Graphify-Labs#3191: an authored markdown link whose target basename is present in the graph but under a different spelling (e.g. Windows-case [[target]] vs TARGET.md) was dropped on a code-only rebuild. Preserve such an edge when its target node is still live in the merged graph, without loosening the deleted-target existence gate (a genuinely deleted target's node is evicted first, so it is never preserved).
On the default incremental path a semantic chunk that returned successfully but under-produced (prior semantic count > 1, fresh < prior) bypassed every guard, silently shrinking the graph. Flag that per-source shrink (computed only from fresh nodes, so a genuinely deleted file is never flagged) and route it through the existing shrink-guard refusal + manifest un-stamp so the source re-extracts next run; --allow-partial overrides. Does not touch the prune path.
… into one helper (Graphify-Labs#3203) The raw and clustered write paths carried a byte-identical ~27-line block differing only in whether the flag is read from merged or G.graph. Extract _handle_unverified_semantic_shrink so the two paths cannot drift; behavior-identical (the Graphify-Labs#3203 and prune-sweep suites pass).
…Labs#3192) Add a Robot Framework extractor using the official robot.api parser (pure-Python, optional [robot] extra), emitting suite/test/keyword nodes plus keyword-call, resource-import, and library edges. Keyword resolution is case/space/underscore-insensitive (RF's real semantics) and handles BDD prefixes and ${CURDIR}/${EXECDIR} path resolution; degrades to an actionable error when robotframework is absent.
…ollision (Graphify-Labs#3192) A user keyword named like an imported library (e.g. a keyword 'SeleniumLibrary') would mint the same id as the library stub. Namespace the stub id under robot_library so the two cannot collide; the display label keeps the raw library name.
…fy-Labs#1790) A plain JavaScript `class Dog extends Animal` produced no inherits edge — only the TypeScript extends_clause/implements_clause shapes were handled, not the bare identifier directly under class_heritage that JS emits. Fall back to reading heritage identifiers when no clause is present, so JS class inheritance is captured; a dynamic base (mixin call) yields nothing and TS is not double-emitted.
…raphify-Labs#1791) Only class_declaration was treated as a class type, so PHP interfaces, enums, and traits produced no node and no heritage edges. Add them as class-like containers (and walk the enum body) so their extends/implements/use heritage is captured as inherits/implements/ mixes_in edges to the right targets.
…hify-Labs#1794) A qualified Scala base (`extends pkg.Base with other.Trait`) parses as a stable_type_identifier (or generic_type wrapping one), which the heritage handler did not match, so the edge was dropped or targeted the full qualified string. Add a helper that takes the tail type name across all three shapes, emitting inherits for the first parent and mixes_in for the rest, with no package/generic-arg false edges.
…phify-Labs#1793) A qualified Kotlin supertype (`class Foo : com.example.Base()`) resolved to the first identifier (`com`), collapsing distinct bases onto a bogus node. Take the last segment of the user type (handling generics like `Container<String>` -> `Container`), so the edge targets the real base; simple and delegated supertypes are unaffected.
Ships two batches: the robot/defang/watch/semantic-guard set — Robot Framework extractor (Graphify-Labs#3192), generalized control-token defang (Graphify-Labs#3183), watch unresolved-link preservation (Graphify-Labs#3190), unverified-semantic-loss guard (Graphify-Labs#3203), hook-guard search detection (Graphify-Labs#3121), stale-SKILL.md backup (Graphify-Labs#3144), report/wiki count fixes (Graphify-Labs#3148/Graphify-Labs#3127); and a rescued batch of @Synvoya cross-language inheritance-edge corrections (JS Graphify-Labs#1790, PHP Graphify-Labs#1791, Scala Graphify-Labs#1792/Graphify-Labs#1794, Kotlin Graphify-Labs#1793, C# Graphify-Labs#1817, Go Graphify-Labs#1818) that had been buried in the backlog for ~7 weeks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ro before parsing (Graphify-Labs#3337) Two defects that made browser entry points invisible in the graph. `window.onWidgetOk = () => {}` produced no node. `_js_member_assignment_target` materializes only `exports.X` and `Foo.prototype.X`; an arbitrary receiver is dropped by the Graphify-Labs#1077 phantom-god-node guard. That guard is right in general and wrong for the global object: assigning a function to `window`/`globalThis`/`self` is how browser code publishes an entry point that a third party invokes by name (Turnstile, reCAPTCHA, Google Maps) or an inline handler calls. The call site is outside the module graph by construction, so the definition already has no incoming edge — dropping the node too makes live code read as absent. The collision Graphify-Labs#1077 warns about is answered by the id rather than by the drop: `_make_id(stem, receiver, member)` keeps two files publishing the same global name as two nodes, the way the `exports` branch already qualifies with the stem. Both call sites are covered — module level, and inside the setup function where the publish usually lives. `extract_astro` handed the whole file to the JS grammar, so tree-sitter errored on the template and the `<script>` symbols survived only as far as error recovery carried them. `.vue` already blanks every non-script region before parsing; `.astro` has the same shape, so it gets the same treatment via `_astro_mask_non_script`. The static-import regex rescue stays, because it is what mints the stub node for a specifier that resolves to nothing on disk, and the edges are deduplicated now that both passes can reach the same import. One trap worth naming: scanning for `<script>` across the whole file matches the one inside a frontmatter comment ("do not duplicate the labels inside the <script>"), which then closes at the real `</script>` hundreds of lines below and blanks the entire client-side region. The scan starts after the frontmatter, and a test holds that shape. Measured on a production Astro site: NeonTerminal.astro 20 -> 31 nodes, Servizi.astro 9 -> 17, both with their Turnstile callbacks, and the syntax-error warning on both files gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWCR6BszPGkdHZXoUijUKj
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. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_\_getattr\_\_ changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_\_getattr\_\_ 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 \{"name":"'save\_query\_result'"\}, the old code produced raises AttributeError but the new code produces \<function save\_query\_result at 0x7fe7552485e0\>. Paste that input straight into a regression test.
Behavior changes: run\_benchmark changes behavior, here is the input that shows it.
The verifier found a concrete input on which run\_benchmark 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 \{"graph\_path":"None","corpus\_words":"None","questions":"None"\}, the old code produced raises TypeError but the new code produces \{'corpus\_tokens': 1045400, 'corpus\_words': 784050, 'nodes': 15681, 'edges': 29262, 'avg\_query\_tokens': 6416, 'reduction\_ratio': 162\.9, 'per\_question': \[\{'question': 'how does authentication…. Paste that input straight into a regression test.
Graphify review — findings
Adds CI, publishing, and release automation: a CI workflow that runs the tests on Python 3.10/3.12, guards that the skillgen-generated files are current, runs the coverage/schema/round-trip validators against origin/v8 on full-history checkouts, and runs bandit/pip-audit non-blocking; a trusted-publishing workflow that builds and uploads to PyPI on release only when the pyproject version matches the tag; and a release-graph workflow that extracts graphify's own AST-only graph and attaches it as a release asset. Adds a local pre-commit config wiring the same skillgen --check anti-drift guard plus ruff, and tightens .gitignore/.dockerignore/.gitattributes to keep generated skill bundles tracked, exclude build/benchmark artifacts, and stop worked/ HTML from skewing Linguist's language detection.
Worth a look
- Manual PyPI publish bypasses release/tag guard —
.github/workflows/publish.yml:17· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- OIDC PyPI publish job uses unpinned third-party actions —
.github/workflows/publish.yml:55· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Release asset workflow grants write token to unpinned actions —
.github/workflows/release-graph.yml:20· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- README gives contradictory default contract for query logging —
README.md· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- README gives contradictory default for query logging —
README.md· 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 — 15331 functions depend on the 15331 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 113 callers, 50 callees - new:
build_from_json()— 192 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 63 callers, 21 callees - new:
build_merge()— 62 callers, 13 callees - new:
save_semantic_cache()— 58 callers, 9 callees - new:
to_obsidian()— 36 callers, 13 callees - …and 220 more — each is listed as a finding
Verification — 15331 functions in the blast radius were not formally verified this run (proofs are advisory here).
Health delta baseline: last indexed commit 33362d9 (diverged from this PR's base — delta is approximate).
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: 15331 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_\_getattr\_\_ changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_\_getattr\_\_ 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 \{"name":"'save\_query\_result'"\}, the old code produced raises AttributeError but the new code produces \<function save\_query\_result at 0x7fe7552485e0\>. Paste that input straight into a regression test.
Behavior changes: run\_benchmark changes behavior, here is the input that shows it.
The verifier found a concrete input on which run\_benchmark 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 \{"graph\_path":"None","corpus\_words":"None","questions":"None"\}, the old code produced raises TypeError but the new code produces \{'corpus\_tokens': 1045400, 'corpus\_words': 784050, 'nodes': 15681, 'edges': 29262, 'avg\_query\_tokens': 6416, 'reduction\_ratio': 162\.9, 'per\_question': \[\{'question': 'how does authentication…. Paste that input straight into a regression test.
No difference found (not proven): No behavior difference found in main (not a proof).
The verifier ran both versions of main 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 \_cross\_community\_surprises (not a proof).
The verifier ran both versions of \_cross\_community\_surprises 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 \_cross\_file\_surprises (not a proof).
The verifier ran both versions of \_cross\_file\_surprises 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 \_file\_category (not a proof).
The verifier ran both versions of \_file\_category 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\_file\_node.
The verifier did not have enough to check \_is\_file\_node, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify \_surprise\_score.
The verifier did not have enough to check \_surprise\_score, 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 TypeError — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in god\_nodes (not a proof).
The verifier ran both versions of god\_nodes 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 graph\_diff.
The verifier did not have enough to check graph\_diff, 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 25 values but only 5 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 suggest\_questions.
The verifier did not have enough to check suggest\_questions, 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 ValueError — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in \_query\_subgraph\_tokens (not a proof).
The verifier ran both versions of \_query\_subgraph\_tokens 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 print\_benchmark.
The verifier did not have enough to check print\_benchmark, 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 KeyError — names the real obstacle, not a sampling gap)
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 cache\_dir.
The verifier did not have enough to check cache\_dir, 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 cached\_files.
The verifier did not have enough to check cached\_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
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 clear\_cache.
The verifier did not have enough to check clear\_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 file\_hash.
The verifier did not have enough to check file\_hash, 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\_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\_cached.
The verifier did not have enough to check save\_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
No difference found (not proven): No behavior difference found in \_split\_community (not a proof).
The verifier ran both versions of \_split\_community 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 cluster.
The verifier did not have enough to check cluster, 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: non-vacuity: domain too small (only 2 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Closes #3337.
Two defects that made browser entry points invisible in the graph. They compound: fixing either one alone still leaves the Turnstile callbacks in a real
.astrocomponent unfound.1. Functions published on a global receiver
window.onWidgetOk = () => {}produced no node._js_member_assignment_targetmaterializes onlyexports.XandFoo.prototype.X; an arbitrary receiver returns("object", …)and is dropped by the #1077 phantom-god-node guard.That guard is right in general and wrong for the global object specifically. Assigning a function to
window/globalThis/selfis how browser code publishes an entry point — the thing adata-callback=attribute or a third-party script (Turnstile, reCAPTCHA, Google Maps) invokes by name. The call site is outside the module graph by construction, so the definition already has no incoming edge; dropping the node too makes live code read as absent.The collision #1077 warns about is answered by the id rather than by the drop:
_make_id(stem, receiver, member)keeps two files publishing the same global name as two separate nodes, the same way theexportsbranch already qualifies with the stem. There is a test for that.test_extract_js_arbitrary_member_assignment_not_captured— the existing #1077 guard — stays green: an ordinaryobj.x = fnis still dropped.Both call sites are covered: module level, and inside the setup function where the publish usually lives (there the node hangs off the publishing function, which is where a reader looks for it).
2.
.astroparsed unmaskedextract_astrohanded the whole file to the JS grammar, so tree-sitter errored on the template and the<script>symbols survived only as far as error recovery carried them..vuealready blanks every non-<script>region before parsing;.astrohas the same shape, so_astro_mask_non_scriptgives it the same treatment (nolangsniff — Astro frontmatter is always TypeScript).The static-import regex rescue stays, because it is what mints the stub node for a specifier that resolves to nothing on disk — dropping it broke
test_astro_unresolved_relative_import_id_still_portable. Both passes can now reach the same import, so the edges are deduplicated at the end.One trap, with a test on it. Scanning for
<script>across the whole file matches the one written inside a frontmatter comment:That phantom block "closes" at the real
</script>580 lines below and blanks the entire client-side region — I hit exactly this on a real component, and it made the file worse than before the change (9 nodes → 7). The scan now starts after the frontmatter.Measured
Three files from a production Astro site, before → after:
NeonTerminal.astrowindow.askTurnstileOk,window.askTurnstileErrServizi.astrowindow.svcTurnstileOk,window.svcTurnstileErrworker/sentry.jsglobalThis.__SEGNALA_SENTRY__warning: … had syntax errors and may be partially extractedon both.astrofiles: gone.Tests
5 added — the two receiver cases (module level, inside a setup function), file-scoped ids,
.astroscript symbols, and the frontmatter-comment regression. Each was written to fail before the corresponding change.Full suite: same 24 pre-existing failures before and after (
test_terraform.py,test_skillgen.py— they fail on a clean checkout in this environment too), passing 4913 → 4918.🤖 Generated with Claude Code
https://claude.ai/code/session_01JWCR6BszPGkdHZXoUijUKj