Skip to content

feat: add F# extractor (.fs/.fsx) via ionide tree-sitter-fsharp - #3221

Open
pcshrosbree wants to merge 1 commit into
Graphify-Labs:v8from
pcshrosbree:feat/fsharp-extractor
Open

pcshrosbree wants to merge 1 commit into
Graphify-Labs:v8from
pcshrosbree:feat/fsharp-extractor

Conversation

@pcshrosbree

@pcshrosbree pcshrosbree commented Aug 30, 2026 •

Copy link
Copy Markdown

TL;DR: graphify has no extractor for F# files (.fs, .fsx), so it skips them without an error; this PR adds one as an optional [fsharp] extra. F# joins the .NET family, so an F# reference can link to a C# definition. Calls are resolved by a lexical model of F#'s name resolution, not by the compiler, and its known gaps are listed below. Because GLSL shaders and Forth also use .fs, such a file goes to F# only when it looks like F#.

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 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. Calls include pipelines (|>, <| and their 2/3-ary forms) and composition (>>, <<).

.NET integration. .fs/.fsx join the dotnet family in all three family maps, so an F# reference can rewire onto a C# definition.

  • Namespaces use the C# canonical namespace id, so a namespace declared in N files is one node.
  • open mirrors using: an imports edge carrying target_fqn.
  • The .NET imports repoint (_resolve_cross_file_csharp_imports) now also runs when the corpus has no .cs file, so the open edges of a pure-F# repository reach the canonical namespace node the F# files declare.
  • The C# type-reference resolver stays C#-only, because its metadata contract (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.

  • Lookup. Names are looked up from the innermost scope outward. Within a scope the latest visible declaration or preceding open wins (a later open shadows an earlier module, an inner open outranks an outer one). An open's own target is resolved through the environment at the open, so open A; open Lib opens A.Lib and an open never reaches a module declared after it. A declaration is visible only after it, inside itself only under let rec, and everywhere in a module rec.
  • Shadowing. Parameters (including those of return-annotated function and member heads, and of property accessors), local let/use, let!/and! groups (for their continuation), lambda, match, for and as binders, and member receivers shadow, and a call to a local binding is not recorded.
  • Qualified calls. Q.f resolves Q'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 own f along the rest of Q. this.M binds to the enclosing type's member M when this is the member's receiver.
  • Scoped stubs. When the answer is in another file, or an open of 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_nodes binds it only to a unique definition with that exact name (the label key drops punctuation, so f' would otherwise match f), never one in the referring file (which already decided), in the container the name reaches from the first scope that has any: Lib.map reaches module Lib's map (Two.Lib's under open One; open Two), List.map reaches nothing in the corpus, and an unqualified map only a map in a module the caller opens or is inside. F# definitions carry metadata.container_path; C# types match on metadata.namespace, never for nested types. This is the one change to shared code, and only stubs carrying that metadata take the new path.
  • Foreign opens in front of a local answer. When an open of another file is nearer than a local binding, the stub lists only those opens, is marked exhaustive, and carries the local answer as its fallback (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; an open type T among 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 an open, say) ends the search.
  • Fail-closed stubs. Receiver calls, and qualifiers that name a container of this file which does not own the name, get a metadata-free stub that never binds.
  • Ids and names. Definition ids keep the exact spelling (run/Run, f/f' and ``a-b``/a_b are distinct). Destructuring binds variables, not constructors or field labels (let (Some v) = … defines v; as aliases 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 to f.
  • xs[i] (F# 6 index syntax, no space) parses as an application of xs to [i]. It resolves as a call only to a same-file let f x = … function declaration (f[1; 2] compiles as a call); a function value such as let f = List.sum is not recognised, so that form is dropped. f [i] with a space and f[|…|] with an array are ordinary applications.
  • g &v 1 and g &&v parse the address-of operator as binary. The call is to g, including after an infix (1 + g &v).

.fs is shared with GLSL fragment shaders and Forth. _get_extractor sniffs the first 64 KB with comments (F# (* *), //, GLSL /* */) and string literals blanked. Strings inside F# comments are lexed as F# does. A .fs file routes to F# only if some line starts with an F# declaration and no line is a GLSL #version/#extension directive. A type line counts only with an identifier type name and the = or with every F# type declaration has, and not when it ends in ;, because Forth's type word 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 starting let can still route to F#. It mirrors the .m Objective-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

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

Verification & Invariants

The invariants this protects:

  • Provenance. A type annotation, generic parameter, constraint type, extended foreign type (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.
  • Extracted versus inferred. A call binds EXTRACTED only to a definition the lexical model reaches, as above; a method call on a value of unknown type is left unresolved, never guessed. Everything else is an INFERRED stub, and a scoped stub can bind in the corpus only inside the container its name reaches from the caller's scopes. The model's known gaps (below) can still produce a wrong edge in the cases they describe.
  • Fail closed on .fs, heuristically. A .fs file 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.py has one named test per wiring point: dispatch for .fs and .fsx, CODE_EXTENSIONS, watch, the extra hint, LANGUAGE_EXTRACTORS, and the three family maps for both extensions.

  • Fixture. tests/fixtures/sample.fs with tests in tests/test_languages.py.

  • Units. tests/test_fsharp.py has 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.py holds 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.

    • Qualified calls. 1,701 such calls are qualified at the call site, and all but one pass that check. The one exception (module internal, which the check does not parse) I verified by hand.
    • False binds removed. Measured the same way, the previous head of this PR landed 637 qualified calls in a container the qualifier does not name (List.map onto a test file's map value, Assert.Single onto an unrelated C# Single). Across the whole graph that included 327 false F#→C# edges.
    • Grammar corrections. Measured before the resolution changes, the index and infix rules added 115 calls (applications right of an operator and address-of arguments) and removed 179 (index reads that were recorded as calls).
    • Scoped unqualified calls. Moving unqualified calls from corpus-wide unique-name binding to scoped binding gained 126 cross-file binds (helpers whose names were ambiguous corpus-wide, now reached through the caller's 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 v8 no 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 clear graphify-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.

  • Read the CONTRIBUTING.md guide.
  • Reproduced the issue and identified the invariant.
  • Made the smallest fix necessary.
  • Added a regression test (if bug fix) or isolated boundary test.
  • Kept the PR description synchronized with the final implementation.
  • Documented any limitations / unsupported cases explicitly.

Known limits (all in the module docstring; the first three also in the README row):

  • .fsi signature files are not extracted. language_signature() exists in the grammar and is a follow-up.
  • #r/#load script directives produce no edges.
  • Calls to static members of C# classes (Widget.Create(), or Abs after open 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.
  • A function value applied to an adjacent list (let f = List.sum then f[1; 2]) is not recorded, and a nullary union case used as a let pattern is read as a binder.
  • Resolution is lexical: no type inference, so a method call on a value of unknown type stays unresolved, and what another file declares is known only from its graph nodes. Two known gaps can produce a wrong INFERRED cross-file edge: an open is not resolved through earlier opens across files (open A; open Lib with A.Lib in another file may bind a root-level Lib elsewhere; within one file it resolves correctly), and accessibility is not modelled (a private function in an opened module of another file can be chosen over a public one further out).

How was this tested?

uv sync --all-extras --frozen                       # Python 3.12
uv run pytest tests/ -q                             # 6,828 passed, 14 skipped, 0 failed (clean extraction cache)
uv run pytest tests/test_fsharp.py tests/test_fsharp_registration.py tests/test_languages.py -q
                                                    # 667 passed on 3.12 and on 3.10
uv run python tests/fsharp_coverage_oracle.py       # 59/59 constructs covered
uv run ruff check .                                 # All checks passed
uv run pre-commit run --files <changed files>       # skillgen --check, ruff: passed
uv run pyright <changed files>                      # no new errors; one existing error on the imports-repoint call moves with it

Graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments. (No fragments touched.)
  • I confirmed that AST/structural extraction remains deterministic (no ambient state dependencies like ENV variables).
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python).
  • I confirmed no API keys or local-only graph data are included.
  • (If applicable) I disclosed AI authorship in my commit messages.

Proposed CHANGELOG line (left out of the diff, since the changelog is curated at release):

Feature: F# .fs/.fsx files are extracted (optional [fsharp] extra, ionide tree-sitter-fsharp): modules, namespaces, types with union cases and members, let-bound functions and values, open imports, heritage, and calls including pipelines. .fs/.fsx join the dotnet family so F# references can rewire onto C# definitions, and module-qualified calls (Lib.map) bind only inside a module the caller can name. A .fs file is routed to F# only when it looks like F# (GLSL shaders and Forth are recognised by a lexical heuristic).

Note

This change was written with AI assistance (Anthropic Claude and OpenAI Codex), and the commit carries Co-Authored-By trailers for both. The verification above was run on this head.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 3 advisory finding(s) below merit a look before merge.


Graphify review — findings

Adds 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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — 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.

Comment thread graphify/extractors/fsharp.py Outdated
parts = identifiers_of(tn)
return (parts[-1] if parts else None), line_of(tn)

def emit_union_cases(defn, type_nid: str) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 15 callees (efferent coupling).

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

@pcshrosbree

Copy link
Copy Markdown
Author

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. emit_member keyed node ids by (file, member) only, so two types in one file each having a Dispose collapsed into a single node. Member ids now carry the owning type's label (_make_id(stem, owner_label, mname)); on a real mixed Rust/C#/F# corpus this un-merged 18 member nodes. Regression test added (test_same_named_members_of_different_types_stay_distinct).

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 application_expression's first named child is a long_identifier_or_op (bare or dotted name). When it's something else — a nested application, as in curried f x y which parses as application(application(f, x), y) — the outer node is deliberately skipped and the walk descends to the inner application, which does carry the long_identifier_or_op head. Recording at the outer node too would double-count the same call site. Parenthesized/lambda heads ((fun x -> ...) arg) genuinely produce no edge, which matches how the OCaml extractor treats them.

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

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.


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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — 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.

Comment thread graphify/extractors/fsharp.py Outdated
parts = identifiers_of(tn)
return (parts[-1] if parts else None), line_of(tn)

def emit_union_cases(defn, type_nid: str) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — emit_union_cases()

fans out to 7 callees (efferent coupling).

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

Comment thread graphify/extractors/fsharp.py Outdated
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 15 callees (efferent coupling).

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

@pcshrosbree

Copy link
Copy Markdown
Author

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:

  • Dotted callees were dropped entirely (application > dot_expression): Grasp.Telemetry.init args, System.IO.File.ReadAllText p, and x |> Some.Deep.fn emitted no call edge. Fixed; on a real 100-file F# corpus this adds +381 raw call edges.
  • let rec f ... and g ...: only the first head was minted — g vanished into a rewireable stub and its body's calls mis-attributed to f (false f → f self-loop). All and-joined heads now mint, with per-binding call attribution.
  • Namespaces are now canonical: namespace Grasp.Core emits the same csharp_namespace:<sha1> id as the C# extractor with type: "namespace", so multi-file namespaces merge into one hub, _canonicalize_csharp_namespace_nodes applies, and the unique-stub rewire skips them. Namespace segments also no longer whitelist local qualified-call binding — under namespace Grasp.Sidecar, Sidecar.validate c stays a stub instead of falsely binding to a same-file validate with EXTRACTED confidence.
  • Member labels use the dotnet .Name() convention, so F# members participate in the same rewire indexing as C# methods instead of posing as type-like unique-stub targets.
  • Union cases are type-scoped (two same-file Ok cases stay distinct) and the canonical single-case wrapper type Email = Email of string no longer emits a contains self-loop.
  • Enum members (enum_type_cases) are emitted; previously only DU cases were.
  • A comment inside a pipeline (x // note newline |> f) no longer kills the call edge (comment nodes are filtered before operand counting).
  • Destructuring let (a, b) = ... mints one definition per bound name instead of none.
  • .fs vs GLSL: .fs is also the fragment-shader extension; dispatch now sniffs (mirroring the .m ObjC/MATLAB precedent) and returns no extractor for shaders instead of ERROR-parsing them into sourceless dotnet-family stubs. Maintainer judgment welcome on the marker list.
  • Registration tests no longer module-level importorskip: the pure-literal registry guards run even without the grammar; only the two dispatch tests skip individually.

State after this commit: 30 F#-specific tests, every registration mutation-killed, full suite green apart from the two failures that reproduce on clean v8, and 20/20 randomly sampled corpus call edges verified at their cited file:line.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

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

Formal verification. 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).

Comment thread graphify/extract.py
return any(m in head for m in fsharp_markers)


def _get_extractor(path: Path) -> Any | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — _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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — 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.

Comment thread graphify/extractors/fsharp.py Outdated
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — emit_cases()

fans out to 7 callees (efferent coupling).

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

Comment thread graphify/extractors/fsharp.py Outdated
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 15 callees (efferent coupling).

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

@pcshrosbree

Copy link
Copy Markdown
Author

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:

  • Generic type names were wrong everywhere: type Box<'T> minted a node labelled T (the last type parameter — type_name carries type_arguments as siblings of the name), and a when 'T :> System.IDisposable constraint minted a sourced IDisposable — the same corpus-poisoning hazard round 2 fixed for annotated lets, resurfacing on a new path. Three of four arms found this independently.
  • type X with impersonation (all four arms): a type augmentation minted a sourced node for the foreign type it extends; verified end-to-end that a C# class Foo : Widget rewired its inherits edge onto the F# extension file. Members now attach to a sourceless stub.
  • Heritage edges now exist: inherit Base() → inherits, interface I with → implements (INFERRED, to stubs). Previously nothing was emitted, so _rewire_unique_stub_nodes's supertype guard could never protect an F#-referenced base type — and F# was absent from the 0.9.53 dotnet-family heritage work despite joining the family.
  • Object expressions (all four arms): { new IFoo with member this.Dispose() = ... } minted a phantom module-level member and — the sharpest version of the finding — poisoned local_defs so a later real same-named binding turned ambiguous and unresolvable. Members are now anonymous; body calls attribute to the enclosing binding.
  • Active patterns and operators (all four arms): let (|Even|Odd|) and let (+.) minted nothing and mis-attributed their body calls to the enclosing module. Both now mint (delimited spelling as label — ends in ), so excluded from the type-rewire by construction).
  • Definition ids now chain from their container (C#'s parent-id convention, with kind tags): sibling modules' same-named run bindings, and the companion type Config + module Config idiom, previously merged into single nodes with unioned call attribution.
  • Function labels use the engine name() convention — a bare-labelled F# function passed _is_type_like_definition and could absorb e.g. a Python parse() stub in the unique-stub rewire (_node_label_key strips punctuation, so cross-file matching is unaffected). Worth noting: ocaml.py has the same bare-label property; pre-existing, not touched here.
  • open now mirrors _import_csharp: EXTRACTED imports edge from the file node to the full-FQN id with target_fqn metadata. The old last-segment stub let open System.Text rewire onto an unrelated class named Text (reproduced), and F# opens were invisible to passes keyed on relation == "imports".
  • GLSL sniff hardened + its test made mutation-killable: a modern no-#version shader (in vec3 / out vec4 / void main()) sailed past the markers via the bare // F# marker; and one arm proved the guard test passed even with the guard deleted. New markers, and a test whose shader deliberately carries an F# marker inside a comment so removing the GLSL check flips it.

State: 40 F#-specific tests; every registration mutation-checked and killed; 20/20 randomly sampled corpus call edges verified at their cited file:line; heritage edges confirmed appearing on a real mixed corpus (5 inherits / 3 implements from F# sources); full suite green apart from the pre-existing v8 failures.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 3 advisory finding(s) below merit a look before merge.

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

Comment thread graphify/extract.py
return any(m in head for m in fsharp_markers)


def _get_extractor(path: Path) -> Any | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — _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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 16 callees (efferent coupling).

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

@pcshrosbree

Copy link
Copy Markdown
Author

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 — tests/fsharp_coverage_oracle.py, 58 F# constructs with presence and commission expectations (forbidden-label sets, self-loop checks; run it with uv run python tests/fsharp_coverage_oracle.py); (2) a frozen corpus baseline for row-level differential reconciliation of every fold; (3) five parallel review arms with differentiated lenses (corpus-integration, end-to-end pipeline, test-adequacy/mutation, grammar coverage, plus a no-lens control); (4) cross-examination of contested fix shapes before implementing; (5) fixer≠verifier — each fix re-verified against its original repro by a different arm.

Yield: 13 verified findings, zero false claims across the panel. Highlights:

  • The round-3 GLSL sniff over-corrected: real F# containing // Draws a sample from a uniform distribution was silently dropped whole-file. My first redesign was then killed in cross-examination by a real corpus line starting float count / float total — the shipped version is evidence-tiered (comment-stripped, line-anchored, strong-F#-declarations override weak type-keyword evidence, 64KB window), and all five adversarial cases are pinned as tests.
  • Pure-F# corpora lost every open edge: _resolve_cross_file_csharp_imports is language-agnostic (reads target_fqn metadata) but was gated on a .cs file existing in the corpus; build then silently pruned the dangling edges. The gate now includes .fs/.fsx for the imports pass only — the C# type-reference resolver stays C#-gated because its metadata contract (metadata.namespace, scope_chain, ref_token) is C#-shaped. Documented follow-up: teaching F# nodes that contract is also what cross-language constructor-call resolution (open CSharpLib; Doubler()) needs; it remains unresolved by design in this PR.
  • The object-expression references edge was dead code — wrong child type matched; it fired on 0 of 11 real corpus object expressions. Now fires, with a positive test.
  • Generic heritage was silently dropped (inherit Base<'T>(), interface IComparer<'T> with wrap in generic_type) — the same bug class round 3 fixed for type_name, now with a shared type-reference reader. 2 live recoveries on the test corpus.
  • static let in classes minted nothing (member calls to them dangled on rewireable stubs); class-internal let capacity case-folded onto member .Capacity() via _make_id, deleting a real public member and creating a false self-loop — member ids now carry a kind tag.
  • Composition >>/<< records both operands as callees (deliberately not generalized to custom operators — Kleisli composes functions but bind has a data operand; there is no sound generic rule).
  • Mutation-proofing: a 47-mutant sweep found the round-2 annotated-let regression test vacuous (its own source parses down the other branch — the naive reversion changed 546 corpus edges with every test green), and two whole features (local_containers positive binding, abstract-member emission) deletable without any test noticing. All pinned now.

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 file:line.

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.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 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).

Comment thread graphify/extract.py
return False


def _get_extractor(path: Path) -> Any | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — _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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 17 callees (efferent coupling).

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

@pcshrosbree

Copy link
Copy Markdown
Author

Response to the latest automated review (5 findings) — verified each before acting, per this PR's standing practice:

Refuted with evidence (the two HIGHs): tests/fsharp_coverage_oracle.py is not truncated and contains no dangling if. At the reviewed head: git show fc71a97:tests/fsharp_coverage_oracle.py | python3 -c "import sys; compile(sys.stdin.read(),'o','exec')" → compiles; 250 lines in both the commit and the working tree, file ends with a well-formed main() guard; uv run python tests/fsharp_coverage_oracle.py → 58/58 constructs covered, exit 0. The review's own caveat ("agreed by 2 of 2 members but NOT verified") applies — it looks like the analyzer saw a truncated window of the file.

Accepted and fixed (latest commit):

  • Hardcoded temp dir — real portability bug (dir="/data/tmp" was machine-local); the oracle now uses the system default and runs anywhere, including CI.
  • Missing-grammar test skipped when the grammar is missing — correct and pleasingly ironic: the test fakes the ImportError, yet module-level importorskip skipped it exactly where it mattered. Moved to the non-skipped module.
  • BOM-prefixed .fs rejected — refuted as stated (probed: BOM files dispatch fine, since later lines carry markers), but a single-marker-line BOM file is a real theoretical hole and the hardening is one line: the sniffer now strips a leading UTF-8 BOM. Regression test added.

57 tests + 58/58 oracle green after the fold.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 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).

Comment thread graphify/extract.py
return False


def _get_extractor(path: Path) -> Any | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — _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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 17 callees (efferent coupling).

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

@pcshrosbree

Copy link
Copy Markdown
Author

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: module A = let helper ... / module B = let bad z = B.helper z bound B.helper EXTRACTED to A's helper — the gate checked that the qualifier was some local container, not that the callee belonged to it. Fixed with a per-container member index: qualified calls bind only through the owning container; non-owning or ambiguous qualifiers yield full-path INFERRED stubs. On the real corpus this demoted 85 receiver-qualified calls from false local binds to honest stubs. Regression test added.

Refuted with evidence (including the HIGH):

  • run_one drops accumulated failures / self-edge check raises NameError — run_one accumulates into problems and returns it once, on every path; lab is defined in its scope before use. The oracle runs end-to-end: 58/58 constructs covered, exit 0 — a NameError or dropped-failure path would be visible there.
  • Undefined variable in F# extension test — the cited region is well-formed; the file's 40 tests all pass.

Working as designed: sniffer rejects F# with no strong markers in 64KB — a compilable .fs file must open with a module or namespace declaration (attributes/comments aside, all of which are marker-bearing line-starts), so the no-evidence default of "not F#" cannot reject valid compilable F#; it exists to keep Forth and marker-free shaders out.

58 tests + 58/58 oracle + full suite 5,356/0 after the fold.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 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).

Comment thread graphify/extract.py
return False


def _get_extractor(path: Path) -> Any | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — _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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 18 callees (efferent coupling).

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

@pcshrosbree

Copy link
Copy Markdown
Author

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: read_bytes()[:65536] slurps the file before slicing. Now a bounded read(65536).

Refuted by probe (2): F# interface implementations are never emitted — they are, on every definition shape: probed a record-shaped type R = { X: int } interface System.IDisposable with ... and got the R → IDisposable implements edge; classes and generics are pinned by existing tests (test_heritage_edges_emitted, test_generic_heritage_edges_emitted) and the oracle's interface impl construct. find_all drops nested matches — true of the helper's traversal in isolation, false in effect for its only use: the grammar nests inner destructuring patterns beside identifier_pattern (under paren_pattern/repeat_pattern), never inside one, so let (a, (b, c)) = ... mints all three names (probed; now pinned by test_nested_destructuring_let_mints_all_names, with an intent comment on the helper).

Repeat of a refuted finding (1): self-edge detection raises NameError at fsharp_coverage_oracle.py:250 — identical to the previous round's report, refuted there with compile-and-run evidence; still: uv run python tests/fsharp_coverage_oracle.py → 58/58 constructs covered, exit 0.

Declared deferral (1): .fsi signature files not routed — deliberate v1 scope from the PR description onward (language_signature() exists in the grammar; the follow-up is documented in the module docstring). Happy to wire it in this PR if maintainers prefer.

59 tests + 58/58 oracle green after the fold.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 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).

Comment thread graphify/extract.py
return False


def _get_extractor(path: Path) -> Any | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — _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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 18 callees (efferent coupling).

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

@pcshrosbree

Copy link
Copy Markdown
Author

Latest automated review (5 findings), verified per standing practice:

Real — fixed (1): (* matched as strong F# evidence — probe-confirmed: a Forth .fs file with a stack-effect comment line ((* stack: a b -- a*b )) dispatched to the F# extractor. The marker existed for block-comment license headers, but a compilable F# file always surfaces a real declaration line within the 64KB window, so it was pure downside. Dropped, with regression tests both ways.

Refuted, for the record (both HIGHs): extractor unparsable due to indentation and undefined name at :260 — the file compiles (python3 -c "compile(open('graphify/extractors/fsharp.py').read(),'f','exec')") and all 41 tests in the module pass at the reviewed head; the cited region is the well-formed type_name_parts. Undefined variable in test :291 — same evidence, tests pass.

Repeat (1): the oracle self-edge NameError is now reported a third time unchanged; refuted twice previously with run evidence (58/58 constructs covered, exit 0).

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 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).

Comment thread graphify/extract.py
return False


def _get_extractor(path: Path) -> Any | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — _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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 18 callees (efferent coupling).

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

@pcshrosbree

Copy link
Copy Markdown
Author

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: uniform as STRONG evidence dropped a real F# file whose continuation line calls a function named uniform. The strong tier now holds only shapes impossible as F# line-starts (#version/#extension, layout, typed io decls in vec3-style, void main, gl_); uniform/varying/precision are weak evidence, which never overrides a strong F# declaration. Regression tests both ways; every previously-pinned shader case still rejects; 0 of 100 real-corpus files change classification. (The in vec-specific half of the claim did not reproduce — verbose-syntax in lines don't start with in vec<digit> — but the tightening covers it anyway.)

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 — uv run python tests/fsharp_coverage_oracle.py → 58/58 constructs covered, exit 0, and it exercised exactly these checks to catch real findings in earlier rounds). Per this thread's practice, repeats get this one line and no further litigation.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 3 advisory finding(s) below merit a look before merge.

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

Comment thread graphify/extract.py
return False


def _get_extractor(path: Path) -> Any | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — _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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 18 callees (efferent coupling).

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

@pcshrosbree

Copy link
Copy Markdown
Author

Latest automated review (3 findings):

Real — fixed (1): layout as strong GLSL evidence — probe-confirmed, same word-vs-directive class as the previous round's uniform: an F# continuation line calling a function named layout dropped the file. Demoted to weak evidence (the tier that never overrides an F# declaration); the layout(...) qualifier shader still rejects via void main/weak accumulation. Regression tests both ways; 0 of 100 corpus files change classification.

Repeats (2): type_name_parts returns the function — third phrasing of the :257/:260 claim; the file compiles and 65 tests + the 58-construct oracle (which exercises a dozen type shapes) are green at the reviewed head. Oracle self-edge NameError — fourth report; same run evidence as before (58/58 constructs covered, exit 0).

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 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).

Comment thread graphify/extract.py
return False


def _get_extractor(path: Path) -> Any | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — _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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 18 callees (efferent coupling).

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

@pcshrosbree

Copy link
Copy Markdown
Author

Rounds 8–10 each surfaced one word-shaped sniff marker that was strong enough to drop a real F# file ((*, uniform , layout). Rather than wait for round 12 to find the next one, this round audits the entire remaining strong tier at once, with the grammar itself as the oracle: for each marker, can tree-sitter-fsharp parse a line starting with it?

Every word-shaped marker fails the audit — each is a valid F# line-start:

  • gl_ — ordinary identifier prefix; OpenGL interop code mirrors C names (gl_makeCurrent ctx on a continuation line parses clean);
  • in vec2/3/4 — verbose syntax puts in at a line start (let x = 1.0 / in vec2 x x is valid, and vec2 is a natural constructor-function name in F# math code);
  • out vec2/3/4 — out is not an F# keyword; out vec3 v is a plain application;
  • void main — void is not a lexer-level F# keyword either (let void = 1 parses); void main is a legal application.

Only #version and #extension are grammar-invalid at an F# line start. The strong tier is now exactly those two directives (9b5ca4b); every word-shaped marker is weak, where it still rejects directive-free shader fragments but can never override an F# declaration. Each demotion is pinned by an acceptance test that kills its re-promotion mutant, and the delete-strong-tier mutant is re-anchored on #version (the old carrier test's shader now rejects via the weak path, which would have left that mutant surviving silently). Corpus reclassification: 99/99 real .fs files unchanged, so extractor output is identical. 70 F# tests + 58/58 oracle + full suite green.

On this round's five advisory findings: the weak_glsl 'int '/'bool ' collision claim fails its probe — the weak tier never overrides a strong F# declaration, and a compilable .fs file always carries one in the 64 KB window (probed live: module M + int x/bool.Parse y continuation lines is accepted). The type_name_parts-undefined, oracle-NameError, and test-undefined-variable items repeat entries already refuted in this thread (the cited tests/test_fsharp.py:286 is a line inside an F# string literal; the test runs green, which a NameError would prevent). The coupling-delta inline notes are acknowledged as descriptive: a language extractor's walk() is a dispatch hub by design, matching the shape of the sibling extractors.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 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).

Comment thread graphify/extract.py
return False


def _get_extractor(path: Path) -> Any | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — _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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 18 callees (efferent coupling).

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

@pcshrosbree

Copy link
Copy Markdown
Author

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

@pcshrosbree

pcshrosbree commented Oct 1, 2026 •

Copy link
Copy Markdown
Author

TL;DR: I squashed this PR into one commit, so it can be cherry-picked as a unit, and brought it in line with the new CONTRIBUTING guide and PR template. The new test fixture and a review of the rebased commit found real defects, and each fix now has a regression test. For example, qualified calls such as List.map were linked to unrelated definitions, some of them in C#. Name resolution is rebuilt to follow F#'s scopes and declaration order, still without type inference.

Rebased onto current v8 (1cd9a36) and squashed into a single commit, so it can be cherry-picked as one unit. The conflicts were all on lines where VB.NET had joined the same .NET family maps; both entries are kept. The description above is rewritten to the new PR template and matches this head.

Brought in line with the new CONTRIBUTING and PR template:

  • Fixture tests/fixtures/sample.fs with tests in tests/test_languages.py.
  • F# registered in LANGUAGE_EXTRACTORS.
  • README rows for the extra, the extensions, and the .fs shader/Forth rule.
  • tree-sitter-fsharp pinned to 0.3.11.
  • AI authorship disclosed in the commit's trailers, and (#3221) referenced.
  • The 59-construct coverage inventory now also runs under pytest.
  • The CHANGELOG line moved to the description.

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:

  • Qualified calls rewired onto unrelated definitions. List.map left a bare map stub, which the corpus rewire bound to any unique map in the .NET family. On a private mixed C#/F# codebase (702 files), 637 qualified cross-file calls landed in a container the qualifier does not name, including 327 false F#→C# edges such as Assert.Single bound to an unrelated C# Single. A qualified call now resolves its qualifier from the caller's scopes (enclosing modules and namespaces, preceding opens, the root). When the qualifier names a foreign container, the stub carries {qualifier, name, scopes} metadata, and _rewire_unique_stub_nodes binds it only inside the container the qualifier names from those scopes. On the same corpus, 1,701 qualified cross-file calls now pass a name-based check that the target's file declares the qualifier. Receiver calls and non-owning local qualifiers get a metadata-free stub that never binds.
  • Name resolution rebuilt on F#'s lexical environment. Calls were resolved against one file-wide name table. So in let apply f = f 42 the parameter call bound EXTRACTED to a module-level f, a helper in an unopened module or defined later in the file was treated as visible, a later open did not shadow an earlier module, and a value named like a module (let Lib = C()) was read as a module qualifier. Each scope now records its declarations and opens in source order, lookup follows F#'s precedence and declaration order, and local binders (parameters, including return-annotated and property-setter heads, local let/use, lambda, match, for and as binders, receivers) shadow. An open's target is resolved through the environment at the open itself; a local answer behind a foreign open is kept as a fallback; a value (local or imported) ends module lookup. Cross-file binding compares exact names (f' is not f) and never binds a definition the referring file already rejected. It remains a lexical model, without type inference; two cross-file gaps (open chains across files, and private accessibility) are listed under Known limits in the description.
  • Other resolution defects (all fixed):
    • A.B.f resolved through A alone, and a nested module could answer to a bare qualifier; qualified calls now use the whole path, resolved from scope.
    • run/Run and f/f' collapsed into one node; ids now keep the exact spelling.
    • let (Some v) = … minted a sourced Some, and as aliases were missed; patterns now bind variables, not constructors (a nullary case as the whole pattern is the one listed exception).
    • this.M bound to a same-named type; it now binds to the enclosing type's member, unless this is rebound locally.
    • A non-recursive let f = f 42 hid the outer f; only let rec scopes a name over its own right-hand side.
  • Grammar-level corrections. tree-sitter-fsharp parses a :: f x as (a :: f) x, xs[i] as an application, and g &v with a binary &; the call rules correct for each. Adjacent array arguments (f[|…|]) and native address-of (g &&v) are handled too.
  • .fs sniffing. #version inside an F# comment or string vetoed valid F#, and Forth's type word passed as an F# declaration. The sniffer now lexes comments and strings as F# does before looking at line starts. A type line needs an identifier name and = or with, and must not end in ;. It remains a lexical heuristic.

Simplifications with no behaviour change: the inert weak-GLSL tier and a write-only set are gone.

Verification on this head:

  • Full suite: 6,373 passed / 14 skipped / 0 failed (Python 3.12, clean extraction cache).
  • F# and language tests pass on 3.10.
  • 64/64 guard-deleting mutations fail a named test.
  • ruff and pre-commit clean.

Note

This change was written with AI assistance (Anthropic Claude and OpenAI Codex), and the commit carries Co-Authored-By trailers for both.

pcshrosbree added a commit to pcshrosbree/graphify that referenced this pull request Oct 1, 2026
…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>
@pcshrosbree
pcshrosbree force-pushed the feat/fsharp-extractor branch from 5a5ad06 to e0003eb Compare October 3, 2026 18:14
pcshrosbree added a commit to pcshrosbree/graphify that referenced this pull request Oct 3, 2026
…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>
@pcshrosbree
pcshrosbree force-pushed the feat/fsharp-extractor branch from e0003eb to d807cfc Compare October 6, 2026 15:53
@pcshrosbree

Copy link
Copy Markdown
Author

Rebased onto current v8 (5c7b847, release 0.9.77), still one commit so it can be cherry-picked as a unit. Eight of the files this PR touches also moved on v8 since the last rebase (extract.py, detect.py, build.py, analyze.py, test_languages.py, pyproject.toml, uv.lock, README); all merged without conflict, and the tree is identical to what git merge-tree produces.

Re-measured on the rebased tree, with a clean extraction cache:

  • full suite: 6,828 passed, 14 skipped;
  • the F# test files: 667 passed on 3.12 and on 3.10;
  • coverage oracle 59/59; ruff, pre-commit and skillgen clean; pyright shows no new errors on the changed files against the same files at v8;
  • the 64-mutant guard-deletion harness: 64/64 killed.

One observation on the base, not this PR: uv lock --check fails on v8 itself since release: 0.9.77 bumped pyproject.toml without regenerating uv.lock (the lock still says 0.9.76). I left that line alone so the PR's uv.lock change stays limited to the tree-sitter-fsharp entries.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant