Repository navigation
feat: add F# extractor (.fs/.fsx) via ionide tree-sitter-fsharp - #3221
pcshrosbree wants to merge 1 commit into
Conversation
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 3 advisory finding(s) below merit a look before merge.
Graphify review — findings
Adds an F# extractor (extract_fsharp) behind an optional [fsharp] extra using ionide's tree-sitter-fsharp, pulling modules, namespaces, record/union/class types with union cases and members, let-bound functions/values, open imports, and calls including pipeline application (x |> f, f <| x); it follows the OCaml extractor's sourceless ref-stub and two-pass call-resolution discipline, and returns an empty result with an error when the grammar isn't installed. Wires .fs/.fsx into the dispatch table, code-extension set, per-extension extra mapping, and the dotnet/cs language families so F#→C# references rewire across the seam.
Worth a look
- First named child of application_expression not treated as callee —
graphify/extractors/fsharp.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
- Same-named members in different F# types are merged into one node —
graphify/extractors/fsharp.py:189· 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-call test passes if the call edge is missing entirely —
tests/test_fsharp.py:119· 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 — 3364 functions depend on the 701 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 523 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:
build_merge()— 62 callers, 13 callees - new:
to_obsidian()— 36 callers, 13 callees - new:
save_manifest()— 40 callers, 11 callees - new:
to_json()— 54 callers, 7 callees - …and 86 more — each is listed as a finding
Verification — 3364 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: 2954 function(s) in the blast radius were not formally verified this run
· 4 grounded finding(s) anchored inline below; 90 more finding(s) on lines outside this diff (see the check run).
| _PIPE_LEFT = frozenset({"<|", "<||", "<|||"}) # callee on the left | ||
|
|
||
|
|
||
| def extract_fsharp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_fsharp()
fans out to 7 callees (efferent coupling); 12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| parts = identifiers_of(tn) | ||
| return (parts[-1] if parts else None), line_of(tn) | ||
|
|
||
| def emit_union_cases(defn, type_nid: str) -> None: |
There was a problem hiding this comment.
emit_union_cases()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| add_edge(type_nid, cnid, "contains", line_of(case)) | ||
| register_def(cname, cnid) | ||
|
|
||
| def emit_member(member_defn, type_nid: str) -> str | None: |
There was a problem hiding this comment.
emit_member()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return node | ||
| return None | ||
|
|
||
| def walk(node, container_nid: str, enclosing_value: str) -> None: |
There was a problem hiding this comment.
walk()
fans out to 15 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Thanks for the review — responding to all three findings (addressed in 1129f64): Same-named members in different F# types merged into one node — confirmed, fixed. Qualified-call test passes if the call edge is missing entirely — confirmed, fixed. The test asserted only inside a loop over matching edges — vacuously green when no edge was emitted at all. It now first asserts the edge exists, then checks it stays a sourceless stub. Fair catch: I mutation-checked the registration points but not this test's own assertions. First named child of application_expression not treated as callee — assessed, no change, reasoning below; happy to be corrected with a concrete case. The extractor records a callee only when All 21 F#-related tests green; corpus spot-check re-run after the fix (20/20 sampled call edges verified at their cited source lines). |
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 1 advisory finding(s) below merit a look before merge.
Graphify review — findings
Adds an F# extractor (extract_fsharp) behind an optional [fsharp] extra using ionide's tree-sitter-fsharp, pulling modules, namespaces, record/union/class types with cases and members, let-bound functions/values, open imports, and calls including pipeline forms (x |> f, f <| x); when the grammar isn't installed it returns empty nodes/edges with an error string rather than failing. Registers .fs/.fsx across detection (CODE_EXTENSIONS), dispatch, and the language-family maps so they extract like source and join the dotnet interop family, letting F#→C# references rewire across the seam. Following the OCaml extractor's discipline, cross-file call targets emit sourceless ref-stubs so the corpus rewire can collapse them onto the real definition.
Worth a look
- Type node IDs ignore enclosing modules and collapse same-named types —
graphify/extractors/fsharp.py:256· 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 — 3365 functions depend on the 702 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 523 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:
build_merge()— 62 callers, 13 callees - new:
to_obsidian()— 36 callers, 13 callees - new:
save_manifest()— 40 callers, 11 callees - new:
to_json()— 54 callers, 7 callees - …and 86 more — each is listed as a finding
Verification — 3365 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: 2955 function(s) in the blast radius were not formally verified this run
· 4 grounded finding(s) anchored inline below; 90 more finding(s) on lines outside this diff (see the check run).
| _PIPE_LEFT = frozenset({"<|", "<||", "<|||"}) # callee on the left | ||
|
|
||
|
|
||
| def extract_fsharp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_fsharp()
fans out to 7 callees (efferent coupling); 13 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| parts = identifiers_of(tn) | ||
| return (parts[-1] if parts else None), line_of(tn) | ||
|
|
||
| def emit_union_cases(defn, type_nid: str) -> None: |
There was a problem hiding this comment.
emit_union_cases()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| add_edge(type_nid, cnid, "contains", line_of(case)) | ||
| register_def(cname, cnid) | ||
|
|
||
| def emit_member(member_defn, type_nid: str, owner_label: str = "") -> str | None: |
There was a problem hiding this comment.
emit_member()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return node | ||
| return None | ||
|
|
||
| def walk(node, container_nid: str, enclosing_value: str) -> None: |
There was a problem hiding this comment.
walk()
fans out to 15 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Second hardening pass pushed in 4a3c419 — an independent adversarial review over the diff surfaced ten further findings; each was verified against the live grammar with AST probes before fixing, and each carries a regression test:
State after this commit: 30 F#-specific tests, every registration mutation-killed, full suite green apart from the two failures that reproduce on clean |
There was a problem hiding this comment.
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. No changes could be formally verified in this run.
Graphify review — findings
Adds an optional F# extractor (extract_fsharp, [fsharp] extra via ionide tree-sitter-fsharp) for .fs/.fsx files, capturing modules, namespaces, record/union/class/enum types with union cases and members, let-bound functions and values, open imports, and calls including pipeline application (x |> f, f <| x), with let rec ... and heads all minted for forward-reference resolution and cross-file targets kept as sourceless ref-stubs. Registers .fs/.fsx in the code-extension set, dispatch table, and the dotnet/cs interop families so F#→C# references rewire across the seam, minting namespaces on the canonical csharp_namespace: id and members as type-scoped .Name() labels to match the C# rewire keys. Guards .fs with _looks_like_fsharp_source, which sniffs the file head and declines routing GLSL fragment shaders or Forth to the F# extractor rather than ERROR-parsing them into bindable dotnet stubs.
No blocking issues surfaced. 13 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3380 functions depend on the 717 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 523 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:
build_merge()— 62 callers, 13 callees - new:
to_obsidian()— 36 callers, 13 callees - new:
save_manifest()— 40 callers, 11 callees - new:
to_json()— 54 callers, 7 callees - …and 87 more — each is listed as a finding
Verification — 3380 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: 2970 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_get\_extractor.
The verifier did not have enough to check \_get\_extractor, 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
· 6 grounded finding(s) anchored inline below; 89 more finding(s) on lines outside this diff (see the check run).
| return any(m in head for m in fsharp_markers) | ||
|
|
||
|
|
||
| def _get_extractor(path: Path) -> Any | None: |
There was a problem hiding this comment.
_get_extractor()
fans out to 7 callees (efferent coupling); 28 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| _CALLEE_TYPES = frozenset({"long_identifier_or_op", "dot_expression"}) | ||
|
|
||
|
|
||
| def extract_fsharp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_fsharp()
fans out to 8 callees (efferent coupling); 22 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| parts = identifiers_of(tn) | ||
| return (parts[-1] if parts else None), line_of(tn) | ||
|
|
||
| def emit_cases(defn, type_nid: str, type_name: str) -> None: |
There was a problem hiding this comment.
emit_cases()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| add_edge(type_nid, cnid, "contains", line_of(case)) | ||
| register_def(cname, cnid) | ||
|
|
||
| def emit_member(member_defn, type_nid: str, owner_label: str = "") -> str | None: |
There was a problem hiding this comment.
emit_member()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return out | ||
| return [] | ||
|
|
||
| def mint_binding_head(head, container_nid: str) -> str | None: |
There was a problem hiding this comment.
mint_binding_head()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| call_sites.append((caller, callee, qualifier, | ||
| ".".join(parts), line_of(callee_node))) | ||
|
|
||
| def walk(node, container_nid: str, enclosing_value: str) -> None: |
There was a problem hiding this comment.
walk()
fans out to 15 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Round 3 pushed in e4b1546 — a four-arm adversarial panel (Codex CLI + three Claude models, identical brief, each instructed to execute probes rather than speculate and to tag VERIFIED vs SUSPECTED) over the round-2 head. Nine distinct defects survived cross-verification; every one was independently reproduced against the live grammar before fixing, and each carries a regression test. The highlights, in severity order:
State: 40 F#-specific tests; every registration mutation-checked and killed; 20/20 randomly sampled corpus call edges verified at their cited |
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 3 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds a first-class F# extractor behind an optional [fsharp] extra (ionide tree-sitter-fsharp) that pulls modules, namespaces, record/union/class types with union cases and members, let-bound functions/values, open imports, and calls including pipeline application, and folds .fs/.fsx into the dotnet language family so F#→C# references rewire across the seam. Uses canonical namespace hubs, container-chained ids, and sourceless ref stubs following the OCaml/C# resolution discipline, and emits inherits/implements heritage edges to protect base types during the corpus rewire. Guards the ambiguous .fs extension with a _looks_like_fsharp_source sniff so GLSL fragment shaders and Forth files fall through to no-extractor rather than parsing into bogus dotnet stubs.
Worth a look
- F# sniff false-negative for files lacking recognized markers in first 4KB —
graphify/extract.py:5607· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- GLSL shader misclassified as F# via generic markers —
graphify/extract.py:5620· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Generic F# regression test has no assertions —
tests/test_fsharp.py:286· 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 — 3391 functions depend on the 728 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 523 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:
build_merge()— 62 callers, 13 callees - new:
to_obsidian()— 36 callers, 13 callees - new:
save_manifest()— 40 callers, 11 callees - new:
to_json()— 54 callers, 7 callees - …and 87 more — each is listed as a finding
Verification — 3391 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: 2981 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_get\_extractor.
The verifier did not have enough to check \_get\_extractor, 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
· 6 grounded finding(s) anchored inline below; 89 more finding(s) on lines outside this diff (see the check run).
| return any(m in head for m in fsharp_markers) | ||
|
|
||
|
|
||
| def _get_extractor(path: Path) -> Any | None: |
There was a problem hiding this comment.
_get_extractor()
fans out to 7 callees (efferent coupling); 30 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| _CALLEE_TYPES = frozenset({"long_identifier_or_op", "dot_expression"}) | ||
|
|
||
|
|
||
| def extract_fsharp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_fsharp()
fans out to 9 callees (efferent coupling); 30 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return [], line_of(tn) | ||
| return identifiers_of(name_node), line_of(tn) | ||
|
|
||
| def emit_cases(defn, type_nid: str) -> None: |
There was a problem hiding this comment.
emit_cases()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| add_edge(type_nid, cnid, "contains", line_of(case)) | ||
| register_def(cname, cnid) | ||
|
|
||
| def emit_member(member_defn, type_nid: str) -> str | None: |
There was a problem hiding this comment.
emit_member()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return out | ||
| return [] | ||
|
|
||
| def mint_binding_head(head, container_nid: str) -> str | None: |
There was a problem hiding this comment.
mint_binding_head()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| call_sites.append((caller, callee, qualifier, | ||
| ".".join(parts), line_of(callee_node))) | ||
|
|
||
| def walk(node, container_nid: str, enclosing_value: str) -> None: |
There was a problem hiding this comment.
walk()
fans out to 16 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Round 4 pushed in fc71a97 — this round ran a redesigned review process, and both the process and its yield are worth a maintainer's minute. Process: (1) a mechanical coverage oracle built before any review — Yield: 13 verified findings, zero false claims across the panel. Highlights:
State: 56 F# tests + 58/58 oracle constructs; every registration and new guard mutation-killed; full suite 5,354 passed / 0 failed; the corpus differential against the frozen baseline reconciled row-for-row (+27/−9, each class explained in the commit message); 20/20 sampled call edges verified at cited The oracle file is included in the PR deliberately: it's ~150 lines, runs in seconds with no API calls, and turns the omission-defect class — which dominated all four review rounds — into a mechanical check any future language extractor can copy. Happy to drop it from the diff if you'd rather keep the PR minimal. |
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. No changes could be formally verified in this run.
Graphify review — findings
Adds F# extraction for .fs/.fsx/.fsx files behind an optional [fsharp] extra, wiring extract_fsharp into the dispatch, extension families, and detect's code-extension set so modules, namespaces, record/union/class types with cases and members, let-bound functions/values, open imports, and pipeline calls (|>, <|) are captured, with .fs joining the dotnet family so F#→C# references rewire across the seam. Routes .fs files through _looks_like_fsharp_source, a 64 KB comment-aware sniffer that keeps GLSL fragment shaders and Forth off the F# extractor by weighing strong GLSL directives against strong F# declaration line-starts. Runs the .NET import-repoint pass for pure-F# corpora via _DOTNET_IMPORT_EXTS so open edges survive build's dangling-edge prune, while leaving the C#-shaped type-reference resolver gated on .cs sources only.
Worth a look
- Dangling if makes coverage oracle invalid Python —
tests/fsharp_coverage_oracle.py:244· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Truncated Python file with dangling if body causes SyntaxError —
tests/fsharp_coverage_oracle.py:249· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- BOM-prefixed .fs files are rejected by the F# routing heuristic —
graphify/extract.py:5619· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Oracle requires a nonstandard temp directory —
tests/fsharp_coverage_oracle.py:218· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Missing-grammar test is skipped when the grammar is actually missing —
tests/test_fsharp.py:8· 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 — 3411 functions depend on the 748 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 524 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:
build_merge()— 62 callers, 13 callees - new:
to_obsidian()— 36 callers, 13 callees - new:
save_manifest()— 40 callers, 11 callees - new:
extract_fsharp()— 43 callers, 9 callees - …and 87 more — each is listed as a finding
Verification — 3411 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: 3001 function(s) in the blast radius were not formally verified this run
Formal verification
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 \_get\_extractor.
The verifier did not have enough to check \_get\_extractor, 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
· 6 grounded finding(s) anchored inline below; 89 more finding(s) on lines outside this diff (see the check run).
| return False | ||
|
|
||
|
|
||
| def _get_extractor(path: Path) -> Any | None: |
There was a problem hiding this comment.
_get_extractor()
fans out to 7 callees (efferent coupling); 34 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| _CALLEE_TYPES = frozenset({"long_identifier_or_op", "dot_expression"}) | ||
|
|
||
|
|
||
| def extract_fsharp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_fsharp()
fans out to 9 callees (efferent coupling); 43 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return [], line_of(tn) | ||
| return identifiers_of(name_node), line_of(tn) | ||
|
|
||
| def emit_cases(defn, type_nid: str) -> None: |
There was a problem hiding this comment.
emit_cases()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| add_edge(type_nid, cnid, "contains", line_of(case)) | ||
| register_def(cname, cnid) | ||
|
|
||
| def emit_member(member_defn, type_nid: str) -> str | None: |
There was a problem hiding this comment.
emit_member()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return out | ||
| return [] | ||
|
|
||
| def mint_binding_head(head, container_nid: str) -> str | None: |
There was a problem hiding this comment.
mint_binding_head()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| call_sites.append((caller, callee, qualifier, | ||
| ".".join(parts), line_of(callee_node))) | ||
|
|
||
| def walk(node, container_nid: str, enclosing_value: str) -> None: |
There was a problem hiding this comment.
walk()
fans out to 17 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Response to the latest automated review (5 findings) — verified each before acting, per this PR's standing practice: Refuted with evidence (the two HIGHs): Accepted and fixed (latest commit):
57 tests + 58/58 oracle green after the fold. |
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. No changes could be formally verified in this run.
Graphify review — findings
Adds F# extraction behind an optional [fsharp] extra, wiring .fs/.fsx through extract_fsharp and registering them across the code-extension, language-family, and edge-family tables so F# joins the dotnet interop family for cross-language reference rewiring. Routes .fs through _looks_like_fsharp_source, an evidence-tiered content sniff that skips comments and gives GLSL directives and F# declaration line-starts strong precedence so shader/Forth .fs files fall through to no extractor (unreadable files default to the F# extractor to surface the real error). Extends the .NET import-repoint pass to run for pure-F# corpora via _DOTNET_IMPORT_EXTS so F# open edges survive build's dangling-edge prune, while keeping the C#-shaped type-reference resolver gated on .cs/.razor/.cshtml.
Worth a look
- run_one drops accumulated failures on normal paths —
tests/fsharp_coverage_oracle.py:250· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- _looks_like_fsharp_source rejects valid F# files with no strong markers in first 64KB —
graphify/extract.py:5669· 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 local F# calls resolve by short member name only —
graphify/extractors/fsharp.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
- Self-edge check raises NameError instead of reporting a problem —
tests/fsharp_coverage_oracle.py:250· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Undefined variable in F# extension test —
tests/test_fsharp.py:513· 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 — 3412 functions depend on the 749 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 524 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:
build_merge()— 62 callers, 13 callees - new:
to_obsidian()— 36 callers, 13 callees - new:
save_manifest()— 40 callers, 11 callees - new:
extract_fsharp()— 44 callers, 9 callees - …and 87 more — each is listed as a finding
Verification — 3412 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: 3002 function(s) in the blast radius were not formally verified this run
Formal verification
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 \_get\_extractor.
The verifier did not have enough to check \_get\_extractor, 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
· 6 grounded finding(s) anchored inline below; 89 more finding(s) on lines outside this diff (see the check run).
| return False | ||
|
|
||
|
|
||
| def _get_extractor(path: Path) -> Any | None: |
There was a problem hiding this comment.
_get_extractor()
fans out to 7 callees (efferent coupling); 35 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| _CALLEE_TYPES = frozenset({"long_identifier_or_op", "dot_expression"}) | ||
|
|
||
|
|
||
| def extract_fsharp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_fsharp()
fans out to 9 callees (efferent coupling); 44 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return [], line_of(tn) | ||
| return identifiers_of(name_node), line_of(tn) | ||
|
|
||
| def emit_cases(defn, type_nid: str) -> None: |
There was a problem hiding this comment.
emit_cases()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| add_edge(type_nid, cnid, "contains", line_of(case)) | ||
| register_def(cname, cnid) | ||
|
|
||
| def emit_member(member_defn, type_nid: str) -> str | None: |
There was a problem hiding this comment.
emit_member()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return out | ||
| return [] | ||
|
|
||
| def mint_binding_head(head, container_nid: str) -> str | None: |
There was a problem hiding this comment.
mint_binding_head()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| call_sites.append((caller, callee, qualifier, | ||
| ".".join(parts), line_of(callee_node))) | ||
|
|
||
| def walk(node, container_nid: str, enclosing_value: str) -> None: |
There was a problem hiding this comment.
walk()
fans out to 17 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Response to the latest automated review (5 findings), each verified before acting: Real — fixed in the latest commit, and credit where due: Qualified local F# calls resolve by short member name only is a genuine defect that four adversarial review arms missed and this review caught first. Reproduced: Refuted with evidence (including the HIGH):
Working as designed: sniffer rejects F# with no strong markers in 64KB — a compilable 58 tests + 58/58 oracle + full suite 5,356/0 after the fold. |
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. No changes could be formally verified in this run.
Graphify review — findings
Adds F# .fs/.fsx extraction behind an optional [fsharp] extra, pulling out modules, namespaces, record/union/class types with cases and members, let-bound functions and values, open imports, and calls including pipeline application. Routes .fs/.fsx into the dotnet interop family across detect/analyze/build so F#→C# references rewire across the seam, and generalizes the .NET import repoint to run for F#-only corpora (previously gated on .cs, which silently dropped F# open edges in the dangling-edge prune) while leaving the C#-shaped type-reference resolver gated on C# sources. Guards .fs with _looks_like_fsharp_source, an evidence-tiered sniff that ignores comment-only lines, treats GLSL directives as strong negative and F# declaration line-starts as strong positive within a 64 KB window, so GLSL fragment shaders and Forth aren't mis-routed to the F# extractor.
Worth a look
- F# signature files are not routed to the F# extractor —
graphify/extract.py:5424· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- F# detection reads entire file despite 64KB limit —
graphify/extract.py:5621· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- F# interface implementations are never emitted —
graphify/extractors/fsharp.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
- find_all drops nested matches of the same node type —
graphify/extractors/fsharp.py:211· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Self-edge detection raises NameError —
tests/fsharp_coverage_oracle.py:250· 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 — 3415 functions depend on the 752 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 524 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:
build_merge()— 62 callers, 13 callees - new:
to_obsidian()— 36 callers, 13 callees - new:
save_manifest()— 40 callers, 11 callees - new:
extract_fsharp()— 45 callers, 9 callees - …and 87 more — each is listed as a finding
Verification — 3415 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: 3005 function(s) in the blast radius were not formally verified this run
Formal verification
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 \_get\_extractor.
The verifier did not have enough to check \_get\_extractor, 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
· 6 grounded finding(s) anchored inline below; 89 more finding(s) on lines outside this diff (see the check run).
| return False | ||
|
|
||
|
|
||
| def _get_extractor(path: Path) -> Any | None: |
There was a problem hiding this comment.
_get_extractor()
fans out to 7 callees (efferent coupling); 35 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| _CALLEE_TYPES = frozenset({"long_identifier_or_op", "dot_expression"}) | ||
|
|
||
|
|
||
| def extract_fsharp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_fsharp()
fans out to 9 callees (efferent coupling); 45 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return [], line_of(tn) | ||
| return identifiers_of(name_node), line_of(tn) | ||
|
|
||
| def emit_cases(defn, type_nid: str) -> None: |
There was a problem hiding this comment.
emit_cases()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| register_def(cname, cnid) | ||
| register_member(type_nid, cname, cnid) | ||
|
|
||
| def emit_member(member_defn, type_nid: str) -> str | None: |
There was a problem hiding this comment.
emit_member()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return out | ||
| return [] | ||
|
|
||
| def mint_binding_head(head, container_nid: str) -> str | None: |
There was a problem hiding this comment.
mint_binding_head()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| call_sites.append((caller, callee, qualifier, | ||
| ".".join(parts), line_of(callee_node))) | ||
|
|
||
| def walk(node, container_nid: str, enclosing_value: str) -> None: |
There was a problem hiding this comment.
walk()
fans out to 18 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Response to the latest automated review (5 findings), verified per standing practice: Taken (1): detection reads entire file despite 64KB limit — correct as a micro-issue: Refuted by probe (2): F# interface implementations are never emitted — they are, on every definition shape: probed a record-shaped Repeat of a refuted finding (1): self-edge detection raises NameError at Declared deferral (1): .fsi signature files not routed — deliberate v1 scope from the PR description onward ( 59 tests + 58/58 oracle green after the fold. |
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. No changes could be formally verified in this run.
Graphify review — findings
Adds F# (.fs/.fsx) extraction behind an optional [fsharp] extra, emitting modules, namespaces, record/union/class types with cases and members, let-bound functions/values, open imports, and calls including pipeline application. Routes .fs through _looks_like_fsharp_source, which reads a bounded 64 KB window and uses tiered comment-aware evidence to reject GLSL fragment shaders and Forth while letting BOM-led and expression-heavy F# through. Joins .fs/.fsx to the dotnet interop family across analyze/build/detect so F#→C# references rewire, and widens the import-repoint gate to _DOTNET_IMPORT_EXTS so F#-only corpora keep their open edges instead of losing them to the dangling-edge prune, while the C#-shaped type-reference resolver stays gated on .cs.
Worth a look
- Unexpected indentation makes F# extractor unparsable —
graphify/extractors/fsharp.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- F# type extraction returns undefined name —
graphify/extractors/fsharp.py:260· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Trailing multi-line block-comment start (*) matched as strong F# by prefix —
graphify/extract.py:5648· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Self-edge check raises NameError instead of reporting a problem —
tests/fsharp_coverage_oracle.py:250· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Undefined variable in new F# test —
tests/test_fsharp.py:291· 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 — 3416 functions depend on the 753 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 524 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:
build_merge()— 62 callers, 13 callees - new:
to_obsidian()— 36 callers, 13 callees - new:
save_manifest()— 40 callers, 11 callees - new:
extract_fsharp()— 46 callers, 9 callees - …and 87 more — each is listed as a finding
Verification — 3416 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: 3006 function(s) in the blast radius were not formally verified this run
Formal verification
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 \_get\_extractor.
The verifier did not have enough to check \_get\_extractor, 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
· 6 grounded finding(s) anchored inline below; 89 more finding(s) on lines outside this diff (see the check run).
| return False | ||
|
|
||
|
|
||
| def _get_extractor(path: Path) -> Any | None: |
There was a problem hiding this comment.
_get_extractor()
fans out to 7 callees (efferent coupling); 35 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| _CALLEE_TYPES = frozenset({"long_identifier_or_op", "dot_expression"}) | ||
|
|
||
|
|
||
| def extract_fsharp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_fsharp()
fans out to 9 callees (efferent coupling); 46 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return [], line_of(tn) | ||
| return identifiers_of(name_node), line_of(tn) | ||
|
|
||
| def emit_cases(defn, type_nid: str) -> None: |
There was a problem hiding this comment.
emit_cases()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| register_def(cname, cnid) | ||
| register_member(type_nid, cname, cnid) | ||
|
|
||
| def emit_member(member_defn, type_nid: str) -> str | None: |
There was a problem hiding this comment.
emit_member()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return out | ||
| return [] | ||
|
|
||
| def mint_binding_head(head, container_nid: str) -> str | None: |
There was a problem hiding this comment.
mint_binding_head()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| call_sites.append((caller, callee, qualifier, | ||
| ".".join(parts), line_of(callee_node))) | ||
|
|
||
| def walk(node, container_nid: str, enclosing_value: str) -> None: |
There was a problem hiding this comment.
walk()
fans out to 18 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Latest automated review (5 findings), verified per standing practice: Real — fixed (1): Refuted, for the record (both HIGHs): extractor unparsable due to indentation and undefined name at :260 — the file compiles ( Repeat (1): the oracle self-edge NameError is now reported a third time unchanged; refuted twice previously with run evidence ( |
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. No changes could be formally verified in this run.
Graphify review — findings
Adds a first-class F# extractor for .fs/.fsx files behind an optional [fsharp] extra, wiring extract_fsharp into the dispatch table, CODE_EXTENSIONS, and the dotnet language family so F#→C# references rewire across the interop seam. Routes .fs through _looks_like_fsharp_source, an evidence-tiered 64KB heuristic that skips comments and defers to strong GLSL directives so fragment shaders and Forth files aren't misclassified as F#. Ungates the dotnet import repoint from C#-only so F# open edges survive build's dangling-edge prune, while keeping the C#-shaped type-reference resolver gated on .cs sources.
Worth a look
- in vec / out vec strong_glsl markers can shadow legitimate F# but ordering makes GLSL win incorrectly —
graphify/extract.py:5680· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- type_name_parts returns undefined name instead of type parts —
graphify/extractors/fsharp.py:260· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Interface implementations are never emitted —
graphify/extractors/fsharp.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
- Oracle script never executes its checks —
tests/fsharp_coverage_oracle.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
- run_one never returns accumulated failures —
tests/fsharp_coverage_oracle.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
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3418 functions depend on the 755 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 524 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:
build_merge()— 62 callers, 13 callees - new:
to_obsidian()— 36 callers, 13 callees - new:
save_manifest()— 40 callers, 11 callees - new:
extract_fsharp()— 46 callers, 9 callees - …and 87 more — each is listed as a finding
Verification — 3418 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: 3008 function(s) in the blast radius were not formally verified this run
Formal verification
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 \_get\_extractor.
The verifier did not have enough to check \_get\_extractor, 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
· 6 grounded finding(s) anchored inline below; 89 more finding(s) on lines outside this diff (see the check run).
| return False | ||
|
|
||
|
|
||
| def _get_extractor(path: Path) -> Any | None: |
There was a problem hiding this comment.
_get_extractor()
fans out to 7 callees (efferent coupling); 37 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| _CALLEE_TYPES = frozenset({"long_identifier_or_op", "dot_expression"}) | ||
|
|
||
|
|
||
| def extract_fsharp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_fsharp()
fans out to 9 callees (efferent coupling); 46 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return [], line_of(tn) | ||
| return identifiers_of(name_node), line_of(tn) | ||
|
|
||
| def emit_cases(defn, type_nid: str) -> None: |
There was a problem hiding this comment.
emit_cases()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| register_def(cname, cnid) | ||
| register_member(type_nid, cname, cnid) | ||
|
|
||
| def emit_member(member_defn, type_nid: str) -> str | None: |
There was a problem hiding this comment.
emit_member()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return out | ||
| return [] | ||
|
|
||
| def mint_binding_head(head, container_nid: str) -> str | None: |
There was a problem hiding this comment.
mint_binding_head()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| call_sites.append((caller, callee, qualifier, | ||
| ".".join(parts), line_of(callee_node))) | ||
|
|
||
| def walk(node, container_nid: str, enclosing_value: str) -> None: |
There was a problem hiding this comment.
walk()
fans out to 18 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Latest automated review (5 findings), verified per standing practice: Real — fixed (1): GLSL word-markers can shadow legitimate F# — probe-confirmed for the word class: Repeats of refuted findings (4): type_name_parts undefined name (2nd repeat — file compiles, 63 tests pass), interface implementations never emitted (2nd repeat — probed on class, record, and generic shapes; edges present and test-pinned), and both oracle claims (never executes checks / run_one never returns failures, 4th round of this family — |
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 3 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds F# .fs/.fsx extraction behind an optional [fsharp] extra, capturing modules, namespaces, record/union/class types with cases and members, let-bound functions/values, open imports, and calls including pipeline application, and folds .fs into the dotnet family so F#→C# references rewire across the seam. Routes .fs through _looks_like_fsharp_source, which reads a bounded 64KB window and uses tiered line-start evidence (strong GLSL directives, strong F# declarations, weak type keywords) to keep GLSL fragment shaders and Forth from being parsed as F#, skipping extraction when the file doesn't look like F#. Registers .fs/.fsx across detection, dispatch, and edge-family tables so the C#-namespace import repointing also runs for pure-F# corpora.
Worth a look
- type_name_parts returns the identifiers_of function instead of parsed type name parts —
graphify/extractors/fsharp.py:257· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Valid F# identifiers starting with layout are treated as strong GLSL —
graphify/extract.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
- Self-edge check raises NameError instead of reporting a problem —
tests/fsharp_coverage_oracle.py:249· 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 — 3420 functions depend on the 757 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 524 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:
build_merge()— 62 callers, 13 callees - new:
to_obsidian()— 36 callers, 13 callees - new:
save_manifest()— 40 callers, 11 callees - new:
extract_fsharp()— 46 callers, 9 callees - …and 87 more — each is listed as a finding
Verification — 3420 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: 3010 function(s) in the blast radius were not formally verified this run
Formal verification
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 \_get\_extractor.
The verifier did not have enough to check \_get\_extractor, 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
· 6 grounded finding(s) anchored inline below; 89 more finding(s) on lines outside this diff (see the check run).
| return False | ||
|
|
||
|
|
||
| def _get_extractor(path: Path) -> Any | None: |
There was a problem hiding this comment.
_get_extractor()
fans out to 7 callees (efferent coupling); 39 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| _CALLEE_TYPES = frozenset({"long_identifier_or_op", "dot_expression"}) | ||
|
|
||
|
|
||
| def extract_fsharp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_fsharp()
fans out to 9 callees (efferent coupling); 46 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return [], line_of(tn) | ||
| return identifiers_of(name_node), line_of(tn) | ||
|
|
||
| def emit_cases(defn, type_nid: str) -> None: |
There was a problem hiding this comment.
emit_cases()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| register_def(cname, cnid) | ||
| register_member(type_nid, cname, cnid) | ||
|
|
||
| def emit_member(member_defn, type_nid: str) -> str | None: |
There was a problem hiding this comment.
emit_member()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return out | ||
| return [] | ||
|
|
||
| def mint_binding_head(head, container_nid: str) -> str | None: |
There was a problem hiding this comment.
mint_binding_head()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| call_sites.append((caller, callee, qualifier, | ||
| ".".join(parts), line_of(callee_node))) | ||
|
|
||
| def walk(node, container_nid: str, enclosing_value: str) -> None: |
There was a problem hiding this comment.
walk()
fans out to 18 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Latest automated review (3 findings): Real — fixed (1): Repeats (2): type_name_parts returns the function — third phrasing of the |
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. No changes could be formally verified in this run.
Graphify review — findings
Adds F# .fs/.fsx extraction behind an optional [fsharp] extra, via extract_fsharp, capturing modules, namespaces, record/union/class types with cases and members, let-bound functions and values, open imports, and pipeline-aware calls (|>, <|). Routes .fs/.fsx into the dotnet language family across analyze, build, detect, and extract so F#→C# references rewire across the interop seam, and runs the imports repoint pass on pure-F# corpora too. Guards .fs dispatch with _looks_like_fsharp_source, which reads a bounded 64 KB head (BOM-stripped, comment-aware) and uses tiered evidence — strong GLSL directives win, strong F# declarations override weak type-keyword lines — to keep GLSL fragment shaders and Forth files off the F# extractor.
Worth a look
- type_name_parts returns undefined name instead of parsed type name —
graphify/extractors/fsharp.py:264· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- weak_glsl marker 'int ' / 'bool ' collides with valid F# expression lines and rejects real F# files —
graphify/extract.py:5688· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- F# type-name helper can raise NameError instead of returning extraction data —
graphify/extractors/fsharp.py:260· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Self-edge check raises NameError instead of recording a problem —
tests/fsharp_coverage_oracle.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
- Undefined variable in F# type extension test —
tests/test_fsharp.py:286· 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 — 3422 functions depend on the 759 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 524 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:
build_merge()— 62 callers, 13 callees - new:
to_obsidian()— 36 callers, 13 callees - new:
save_manifest()— 40 callers, 11 callees - new:
extract_fsharp()— 46 callers, 9 callees - …and 87 more — each is listed as a finding
Verification — 3422 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: 3012 function(s) in the blast radius were not formally verified this run
Formal verification
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 \_get\_extractor.
The verifier did not have enough to check \_get\_extractor, 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
· 6 grounded finding(s) anchored inline below; 89 more finding(s) on lines outside this diff (see the check run).
| return False | ||
|
|
||
|
|
||
| def _get_extractor(path: Path) -> Any | None: |
There was a problem hiding this comment.
_get_extractor()
fans out to 7 callees (efferent coupling); 41 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| _CALLEE_TYPES = frozenset({"long_identifier_or_op", "dot_expression"}) | ||
|
|
||
|
|
||
| def extract_fsharp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_fsharp()
fans out to 9 callees (efferent coupling); 46 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return [], line_of(tn) | ||
| return identifiers_of(name_node), line_of(tn) | ||
|
|
||
| def emit_cases(defn, type_nid: str) -> None: |
There was a problem hiding this comment.
emit_cases()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| register_def(cname, cnid) | ||
| register_member(type_nid, cname, cnid) | ||
|
|
||
| def emit_member(member_defn, type_nid: str) -> str | None: |
There was a problem hiding this comment.
emit_member()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return out | ||
| return [] | ||
|
|
||
| def mint_binding_head(head, container_nid: str) -> str | None: |
There was a problem hiding this comment.
mint_binding_head()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| call_sites.append((caller, callee, qualifier, | ||
| ".".join(parts), line_of(callee_node))) | ||
|
|
||
| def walk(node, container_nid: str, enclosing_value: str) -> None: |
There was a problem hiding this comment.
walk()
fans out to 18 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Rounds 8–10 each surfaced one word-shaped sniff marker that was strong enough to drop a real F# file ( Every word-shaped marker fails the audit — each is a valid F# line-start:
Only On this round's five advisory findings: the |
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. No changes could be formally verified in this run.
Graphify review — findings
Adds F# extraction for .fs/.fsx files behind an optional [fsharp] extra, routing them through extract_fsharp and wiring the extension into CODE_EXTENSIONS, the dispatch table, and the per-extension extra map. Places .fs/.fsx in the dotnet interop family across analyze, build, and extract so F#→C# references rewire across the seam. Guards the .fs route with _looks_like_fsharp_source, a tiered 64 KB sniff that skips comment lines, treats #version/#extension as decisive GLSL and F# declaration line-starts as decisive F#, and rejects shader-shaped files when no strong F# evidence appears — defaulting to reject on ambiguity so GLSL/Forth .fs files don't get parsed as F#.
Worth a look
- F# extractor appears to introduce an unexpected indent —
graphify/extractors/fsharp.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- type_name_parts returns undefined name —
graphify/extractors/fsharp.py:262· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- F# interface implementations are not emitted despite contract —
graphify/extractors/fsharp.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
- Self-edge check raises NameError instead of recording a problem —
tests/fsharp_coverage_oracle.py:250· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Undefined variable in F# test assertion —
tests/test_fsharp.py:288· 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 — 3427 functions depend on the 764 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 524 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:
build_merge()— 62 callers, 13 callees - new:
to_obsidian()— 36 callers, 13 callees - new:
save_manifest()— 40 callers, 11 callees - new:
extract_fsharp()— 46 callers, 9 callees - …and 87 more — each is listed as a finding
Verification — 3427 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: 3017 function(s) in the blast radius were not formally verified this run
Formal verification
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 \_get\_extractor.
The verifier did not have enough to check \_get\_extractor, 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
· 6 grounded finding(s) anchored inline below; 89 more finding(s) on lines outside this diff (see the check run).
| return False | ||
|
|
||
|
|
||
| def _get_extractor(path: Path) -> Any | None: |
There was a problem hiding this comment.
_get_extractor()
fans out to 7 callees (efferent coupling); 46 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| _CALLEE_TYPES = frozenset({"long_identifier_or_op", "dot_expression"}) | ||
|
|
||
|
|
||
| def extract_fsharp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_fsharp()
fans out to 9 callees (efferent coupling); 46 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return [], line_of(tn) | ||
| return identifiers_of(name_node), line_of(tn) | ||
|
|
||
| def emit_cases(defn, type_nid: str) -> None: |
There was a problem hiding this comment.
emit_cases()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| register_def(cname, cnid) | ||
| register_member(type_nid, cname, cnid) | ||
|
|
||
| def emit_member(member_defn, type_nid: str) -> str | None: |
There was a problem hiding this comment.
emit_member()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return out | ||
| return [] | ||
|
|
||
| def mint_binding_head(head, container_nid: str) -> str | None: |
There was a problem hiding this comment.
mint_binding_head()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| call_sites.append((caller, callee, qualifier, | ||
| ".".join(parts), line_of(callee_node))) | ||
|
|
||
| def walk(node, container_nid: str, enclosing_value: str) -> None: |
There was a problem hiding this comment.
walk()
fans out to 18 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
For the record on the round-12 HIGH 'F# extractor appears to introduce an unexpected indent': probed at 9b5ca4b — py_compile and tokenize both pass clean on graphify/extractors/fsharp.py (no IndentationError/TabError, zero tab characters), and the full suite (5,367 tests) imports and executes the module. A real unexpected indent would make the module unimportable. The other four findings this round repeat items already refuted above (type_name_parts, the oracle self-edge NameError, interface-impls, and the tests/test_fsharp.py 'undefined variable' — still a line inside an F# string literal; the anchor drifted :286 to :288). |
9b5ca4b to
5a5ad06
Compare
Rebased onto current Brought in line with the new CONTRIBUTING and PR template:
Found and fixed since the last push. The new fixture and an adversarial review of the rebased commit found real defects, each now pinned by a regression test:
Simplifications with no behaviour change: the inert weak-GLSL tier and a write-only set are gone. Verification on this head:
Note This change was written with AI assistance (Anthropic Claude and OpenAI Codex), and the commit carries |
…hify-Labs#3221) F# source files matched no extractor and were skipped without an error. This adds graphify/extractors/fsharp.py behind an optional [fsharp] extra (tree-sitter-fsharp 0.3.11, prebuilt abi3 wheels for Windows, macOS and Linux on x86-64 and ARM64). Extracted: namespaces, modules, record/union/enum/interface/class types, union cases and enum members, class and interface members (including `member val` and abstract members), let-bound functions and values (every head of `let rec ... and ...`, and the names destructuring patterns bind), `static let`, active patterns, operator definitions, exceptions, `open` imports, heritage (`inherit` -> inherits, `interface I with` -> implements), and calls, including pipelines (|>, <| and their 2/3-ary forms) and composition. .fs/.fsx join the dotnet family in all three family maps, so F# references can rewire onto C# definitions. Namespaces use the C# canonical namespace id, and `open` mirrors `using` (an imports edge carrying target_fqn); the .NET imports repoint now also runs for corpora with no .cs file, so F# `open` edges reach the canonical namespace node. Call resolution models F#'s lexical environment: each scope records its declarations and opens in source order; lookup goes from the innermost scope outward, the latest visible declaration or open wins, and declarations are visible only after themselves (inside themselves under `let rec`, anywhere in `module rec`). Local binders shadow. A call binds EXTRACTED only to a definition of this file that this lookup reaches; a value as a qualifier's first segment makes a receiver call. Otherwise the call gets a scoped stub carrying {qualifier, name, scopes}, which _rewire_unique_stub_nodes binds only to a unique definition with that exact name, outside the referring file, in the container the name reaches from the first scope that has any, so `List.map` no longer rewires onto an unrelated `map`. An open's own target is resolved through the environment at the open; a local answer behind a foreign open is carried as the stub's fallback; a value ends module lookup. It is a lexical model, without type inference; its known cross-file gaps (open chains across files, accessibility) are documented in the module docstring. Definition ids keep the exact spelling (`run`/`Run`, `f`/`f'`). Three places where the grammar's tree disagrees with F# semantics are corrected in the call rules: `a :: f x` parses as `(a :: f) x`, `xs[i]` parses as an application of xs, and `g &v` parses & as a binary operator. `.fs` is also used by GLSL fragment shaders and Forth, so dispatch sniffs the first 64 KB with comments and strings blanked: a file is F# only if a line starts with an F# declaration (a `type` line needs `=` or `with`) and no line is a GLSL #version/#extension directive. This is a lexical heuristic. Tests: one test per registration point, fixture tests in test_languages.py, unit tests per construct, resolution rule and grammar correction, and a 59-construct coverage inventory (tests/fsharp_coverage_oracle.py) also run under pytest. Not handled: .fsi signature files; #r/#load script directives; calls to static members of C# classes; [<AutoOpen>] modules. Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: OpenAI Codex <noreply@openai.com>
5a5ad06 to
e0003eb
Compare
…hify-Labs#3221) F# source files matched no extractor and were skipped without an error. This adds graphify/extractors/fsharp.py behind an optional [fsharp] extra (tree-sitter-fsharp 0.3.11, prebuilt abi3 wheels for Windows, macOS and Linux on x86-64 and ARM64). Extracted: namespaces, modules, record/union/enum/interface/class types, union cases and enum members, class and interface members (including `member val` and abstract members), let-bound functions and values (every head of `let rec ... and ...`, and the names destructuring patterns bind), `static let`, active patterns, operator definitions, exceptions, `open` imports, heritage (`inherit` -> inherits, `interface I with` -> implements), and calls, including pipelines (|>, <| and their 2/3-ary forms) and composition. .fs/.fsx join the dotnet family in all three family maps, so F# references can rewire onto C# definitions. Namespaces use the C# canonical namespace id, and `open` mirrors `using` (an imports edge carrying target_fqn); the .NET imports repoint now also runs for corpora with no .cs file, so F# `open` edges reach the canonical namespace node. Call resolution models F#'s lexical environment: each scope records its declarations and opens in source order; lookup goes from the innermost scope outward, the latest visible declaration or open wins, and declarations are visible only after themselves (inside themselves under `let rec`, anywhere in `module rec`). Local binders shadow. A call binds EXTRACTED only to a definition of this file that this lookup reaches; a value as a qualifier's first segment makes a receiver call. Otherwise the call gets a scoped stub carrying {qualifier, name, scopes}, which _rewire_unique_stub_nodes binds only to a unique definition with that exact name, outside the referring file, in the container the name reaches from the first scope that has any, so `List.map` no longer rewires onto an unrelated `map`. An open's own target is resolved through the environment at the open; a local answer behind a foreign open is carried as the stub's fallback; a value ends module lookup. It is a lexical model, without type inference; its known cross-file gaps (open chains across files, accessibility) are documented in the module docstring. Definition ids keep the exact spelling (`run`/`Run`, `f`/`f'`). Three places where the grammar's tree disagrees with F# semantics are corrected in the call rules: `a :: f x` parses as `(a :: f) x`, `xs[i]` parses as an application of xs, and `g &v` parses & as a binary operator. `.fs` is also used by GLSL fragment shaders and Forth, so dispatch sniffs the first 64 KB with comments and strings blanked: a file is F# only if a line starts with an F# declaration (a `type` line needs `=` or `with`) and no line is a GLSL #version/#extension directive. This is a lexical heuristic. Tests: one test per registration point, fixture tests in test_languages.py, unit tests per construct, resolution rule and grammar correction, and a 59-construct coverage inventory (tests/fsharp_coverage_oracle.py) also run under pytest. Not handled: .fsi signature files; #r/#load script directives; calls to static members of C# classes; [<AutoOpen>] modules. Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: OpenAI Codex <noreply@openai.com>
…hify-Labs#3221) F# source files matched no extractor and were skipped without an error. This adds graphify/extractors/fsharp.py behind an optional [fsharp] extra (tree-sitter-fsharp 0.3.11, prebuilt abi3 wheels for Windows, macOS and Linux on x86-64 and ARM64). Extracted: namespaces, modules, record/union/enum/interface/class types, union cases and enum members, class and interface members (including `member val` and abstract members), let-bound functions and values (every head of `let rec ... and ...`, and the names destructuring patterns bind), `static let`, active patterns, operator definitions, exceptions, `open` imports, heritage (`inherit` -> inherits, `interface I with` -> implements), and calls, including pipelines (|>, <| and their 2/3-ary forms) and composition. .fs/.fsx join the dotnet family in all three family maps, so F# references can rewire onto C# definitions. Namespaces use the C# canonical namespace id, and `open` mirrors `using` (an imports edge carrying target_fqn); the .NET imports repoint now also runs for corpora with no .cs file, so F# `open` edges reach the canonical namespace node. Call resolution models F#'s lexical environment: each scope records its declarations and opens in source order; lookup goes from the innermost scope outward, the latest visible declaration or open wins, and declarations are visible only after themselves (inside themselves under `let rec`, anywhere in `module rec`). Local binders shadow. A call binds EXTRACTED only to a definition of this file that this lookup reaches; a value as a qualifier's first segment makes a receiver call. Otherwise the call gets a scoped stub carrying {qualifier, name, scopes}, which _rewire_unique_stub_nodes binds only to a unique definition with that exact name, outside the referring file, in the container the name reaches from the first scope that has any, so `List.map` no longer rewires onto an unrelated `map`. An open's own target is resolved through the environment at the open; a local answer behind a foreign open is carried as the stub's fallback; a value ends module lookup. It is a lexical model, without type inference; its known cross-file gaps (open chains across files, accessibility) are documented in the module docstring. Definition ids keep the exact spelling (`run`/`Run`, `f`/`f'`). Three places where the grammar's tree disagrees with F# semantics are corrected in the call rules: `a :: f x` parses as `(a :: f) x`, `xs[i]` parses as an application of xs, and `g &v` parses & as a binary operator. `.fs` is also used by GLSL fragment shaders and Forth, so dispatch sniffs the first 64 KB with comments and strings blanked: a file is F# only if a line starts with an F# declaration (a `type` line needs `=` or `with`) and no line is a GLSL #version/#extension directive. This is a lexical heuristic. Tests: one test per registration point, fixture tests in test_languages.py, unit tests per construct, resolution rule and grammar correction, and a 59-construct coverage inventory (tests/fsharp_coverage_oracle.py) also run under pytest. Not handled: .fsi signature files; #r/#load script directives; calls to static members of C# classes; [<AutoOpen>] modules. Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: OpenAI Codex <noreply@openai.com>
e0003eb to
d807cfc
Compare
|
Rebased onto current Re-measured on the rebased tree, with a clean extraction cache:
One observation on the base, not this PR: |
What does this PR do?
F# source files (
.fs,.fsx) matched no extractor, so graphify skipped them without an error. This adds an F# extractor behind an optional[fsharp]extra, using ionide's tree-sitter-fsharp pinned to 0.3.11, which ships prebuilt abi3 wheels for Windows, macOS and Linux on x86-64 and ARM64. It follows the pattern of[ocaml]and[commonlisp].Extracted: namespaces, modules, record/union/enum/interface/class types, union cases and enum members, class and interface members (including
member valand abstract members), let-bound functions and values (every head oflet rec … and …, and the names destructuring patterns bind),static let, active patterns, operator definitions, exceptions,openimports, heritage (inherit→inherits,interface I with→implements), and calls. Calls include pipelines (|>,<|and their 2/3-ary forms) and composition (>>,<<)..NET integration.
.fs/.fsxjoin thedotnetfamily in all three family maps, so an F# reference can rewire onto a C# definition.openmirrorsusing: animportsedge carryingtarget_fqn._resolve_cross_file_csharp_imports) now also runs when the corpus has no.csfile, so theopenedges of a pure-F# repository reach the canonical namespace node the F# files declare.metadata.namespace,scope_chain,ref_token) is C#-shaped.Call resolution. This is a lexical model of F#'s name resolution, not the compiler's: it has no type inference and known gaps (listed under Known limits). A call binds EXTRACTED only where the model reaches a definition of the file; anything else stays an inferred stub. Every scope (file, namespace, module, type) records its declarations and
opens in source order.openwins (a lateropenshadows an earlier module, an inneropenoutranks an outer one). Anopen's own target is resolved through the environment at theopen, soopen A; open LibopensA.Liband anopennever reaches a module declared after it. A declaration is visible only after it, inside itself only underlet rec, and everywhere in amodule rec.let/use,let!/and!groups (for their continuation), lambda, match,forandasbinders, and member receivers shadow, and a call to a local binding is not recorded.Q.fresolvesQ's first segment the same way: a value there makes it a receiver call (let Lib = C() … Lib.Map); a module or type there must ownfalong the rest ofQ.this.Mbinds to the enclosing type's memberMwhenthisis the member's receiver.openof another file is nearer than any local match, the call gets a stub labelled with the name as written, carrying{qualifier, name, scopes}(scopes nearest first, and part of the stub's id)._rewire_unique_stub_nodesbinds it only to a unique definition with that exact name (the label key drops punctuation, sof'would otherwise matchf), never one in the referring file (which already decided), in the container the name reaches from the first scope that has any:Lib.mapreaches moduleLib'smap(Two.Lib's underopen One; open Two),List.mapreaches nothing in the corpus, and an unqualifiedmaponly amapin a module the caller opens or is inside. F# definitions carrymetadata.container_path; C# types match onmetadata.namespace, never for nested types. This is the one change to shared code, and only stubs carrying that metadata take the new path.openof another file is nearer than a local binding, the stub lists only those opens, is markedexhaustive, and carries the local answer as itsfallback(container path and exact name), used when none of the opens supplies the name. If the local answer is a value, the call is a receiver call with no fallback; anopen type Tamong the nearer opens also removes the fallback, since T's static members are unknown, so the stub stays unbound rather than restoring a shadowed local function. In the rewire, a scope that supplies a qualifier's first segment as anything but an owning container (a value imported through anopen, say) ends the search.run/Run,f/f'and``a-b``/a_bare distinct). Destructuring binds variables, not constructors or field labels (let (Some v) = …definesv;asaliases bind). A return-annotated head is minted as a function; tree-sitter-fsharp parses it as a value head when it is the last binding in a file and as a function head otherwise, so both are handled.Where the grammar's tree disagrees with F# semantics, the call rules correct for it:
a :: f x(every infix operator except&&/||) parses as(a :: f) x. Application binds tighter, so the call is tof.xs[i](F# 6 index syntax, no space) parses as an application ofxsto[i]. It resolves as a call only to a same-filelet f x = …function declaration (f[1; 2]compiles as a call); a function value such aslet f = List.sumis not recognised, so that form is dropped.f [i]with a space andf[|…|]with an array are ordinary applications.g &v 1andg &&vparse the address-of operator as binary. The call is tog, including after an infix (1 + g &v)..fsis shared with GLSL fragment shaders and Forth._get_extractorsniffs the first 64 KB with comments (F#(* *),//, GLSL/* */) and string literals blanked. Strings inside F# comments are lexed as F# does. A.fsfile routes to F# only if some line starts with an F# declaration and no line is a GLSL#version/#extensiondirective. Atypeline counts only with an identifier type name and the=orwithevery F# type declaration has, and not when it ends in;, because Forth'stypeword and GLSL statements also start lines that way. Word-shaped GLSL markers (uniform,layout,gl_,in vec3,void main) never veto F#, because each is a valid F# line start. This is a lexical heuristic, not a language proof: a Forth parenthesised comment with a line startingletcan still route to F#. It mirrors the.mObjective-C/MATLAB sniff.Registry and docs. F# is added to
LANGUAGE_EXTRACTORS, the README's extras table, the code-extension row and the language table.Type of change
Verification & Invariants
The invariants this protects:
type System.String with) or object-expression interface is never minted as a sourced definition, and neither is a union-case constructor in a pattern, except a nullary case used as the whole pattern (let (None) = …), listed under Known limits..fs, heuristically. A.fsfile that does not pass the lexical sniff gets no extractor, instead of being ERROR-parsed into stubs; the sniff's known false positive is described above.How each is checked:
Registration.
tests/test_fsharp_registration.pyhas one named test per wiring point: dispatch for.fsand.fsx,CODE_EXTENSIONS, watch, the extra hint,LANGUAGE_EXTRACTORS, and the three family maps for both extensions.Fixture.
tests/fixtures/sample.fswith tests intests/test_languages.py.Units.
tests/test_fsharp.pyhas a unit test per construct, resolution rule and grammar correction, with controls where a test would otherwise pass for the wrong reason.Coverage inventory.
tests/fsharp_coverage_oracle.pyholds 59 F# constructs, each with required labels and edges and, where relevant, forbidden labels. It runs as a script and as parametrized pytest cases.Mutation check. 64 mutations, each deleting or inverting one guard added by this PR (the rules above, the sniffer's lexer cases, and the registration entries), were each run against the F# tests on a clean extraction cache; a collection or import error did not count as a kill. All 64 fail a named test. This covers the guards in that list, not every line of the extractor.
Composed check on a real corpus. A private mixed C#/F# codebase (702 files: 131 F#, 571 C#) was extracted and built on a clean cache, and every F#-touching edge was compared across revisions of this PR. The correctness figures below come from a name-based check, not a type checker: for each F# call that lands on a definition in another file, the qualifier written at the call site must name a namespace, module or type the target file declares. That check is independent of the scoping rule under test, but a heuristic.
module internal, which the check does not parse) I verified by hand.List.maponto a test file'smapvalue,Assert.Singleonto an unrelated C#Single). Across the whole graph that included 327 false F#→C# edges.open) and lost 1, whose old target was a C# type bound from a record field label. For every added unqualified bind, a text check confirmed the caller's file opens or encloses the target's container.The corpus results were compared against the call-site source lines: samples for the added and removed calls, and every dropped cross-file bind one by one. The graph's edge count is per caller/callee pair, so these figures count distinct pairs, not call sites.
Persisted state: relative to upstream
v8no F# extraction exists. Anyone who ran an earlier head of this PR has cached F# extractions: the AST cache is keyed by graphify version and file content, so they must cleargraphify-out/cache(or rebuild) to see this head's output. Mixed-language graphs change: cross-language edges from F# files now follow the qualified-stub rule.Known limits (all in the module docstring; the first three also in the README row):
.fsisignature files are not extracted.language_signature()exists in the grammar and is a follow-up.#r/#loadscript directives produce no edges.Widget.Create(), orAbsafteropen type System.Math) stay unresolved, because C# member labels (.Name()) are not rewire targets. A C# constructor call resolves only when its type is reached from the caller's scopes (open Acme; Widget()), and a C# nested type never does.[<AutoOpen>]modules are not recognised: an unqualified call into one binds only if the module is opened explicitly.let f = List.sumthenf[1; 2]) is not recorded, and a nullary union case used as aletpattern is read as a binder.openis not resolved through earlieropens across files (open A; open LibwithA.Libin another file may bind a root-levelLibelsewhere; within one file it resolves correctly), and accessibility is not modelled (aprivatefunction in an opened module of another file can be chosen over a public one further out).How was this tested?
Graphify-specific checklist
uv run python -m tools.skillgen --bless) when changing their source fragments. (No fragments touched.)Proposed CHANGELOG line (left out of the diff, since the changelog is curated at release):
Note
This change was written with AI assistance (Anthropic Claude and OpenAI Codex), and the commit carries
Co-Authored-Bytrailers for both. The verification above was run on this head.