Skip to content

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

Open
pcshrosbree wants to merge 12 commits into
Graphify-Labs:v8from
pcshrosbree:feat/fsharp-extractor
Open

feat: add F# extractor (.fs/.fsx) via ionide tree-sitter-fsharp#3221
pcshrosbree wants to merge 12 commits into
Graphify-Labs:v8from
pcshrosbree:feat/fsharp-extractor

Conversation

@pcshrosbree

Copy link
Copy Markdown

What

F# support: .fs/.fsx files are extracted via ionide's tree-sitter-fsharp (0.3.11, prebuilt abi3 wheels for every platform), as an optional [fsharp] extra — matching the recent precedent of [ocaml]/[commonlisp]/[robot].

Extracted: modules, namespaces, record/union/interface/enum/class types, union cases, class members, let-bound functions and values, open imports, and calls — including pipeline application (x |> f, f <| x, and the 2/3-ary variants), which is where most F# call edges live.

.fs/.fsx join the dotnet interop family in all three family maps (extract._LANG_FAMILY_BY_EXT, analyze._LANG_FAMILY, build._EDGE_LANG_FAMILY), so an F# reference to a C# type rewires onto the real definition instead of dangling.

How

graphify/extractors/fsharp.py follows extractors/ocaml.py closely (F# is ML-family): the same sourceless ref-stub discipline (#1402), the same local-definition table with ambiguity tracking, the same two-pass call resolution, and the same qualified-call gating (a call M.f where M is not defined locally stays a distinct stub keyed by its full path, so List.map can never absorb a local map).

F#-specific handling, grounded in live AST probes of the grammar rather than tags.scm alone:

  • Names are nested, not a field: type_definition > *_type_defn > type_name, function_declaration_left > identifier, value_declaration_left > identifier_pattern > long_identifier_or_op, and method_or_prop_defn > property_or_ident (last identifier — this.Run carries two).
  • Pipe operators are infix_expressions: the callee is the right operand for |>-family, the left for <|-family; both feed the same call-site table as application_expression.

Verification

Beyond the unit tests (tests/test_fsharp.py), two things this PR did that I'd flag for review:

  1. One named test per registration point, mutation-checked. tests/test_fsharp_registration.py covers each wiring point (dispatch ×2, CODE_EXTENSIONS, watch, extra-hint, and all three family maps) individually. I then removed each registration in turn and confirmed a test fails. The first run caught an untested .fsx dispatch entry — the exact silent-skip failure mode that made F# invisible in the first place (a file matching no category produces no error).

  2. Graded on a real mixed Rust/C#/F# repo (a .NET instrumentation platform, ~470k LOC, 100 F# files including a full F# sidecar service): 2,361 sourced nodes, 10,238 call edges; 20 randomly sampled call edges all verified by hand at their cited file:line. Cross-language: 192 C#↔F# edges where the release build produces zero.

The grade also caught a bug the fixture tests missed, now fixed with a regression test: let subscribe (a: A) (b: B) : IDisposable = ... parses as a value_declaration_left whose last identifier is the return-type annotation — the first cut minted a sourced IDisposable node, and the dotnet family rewire then bound every BCL implements IDisposable stub in the corpus onto it (82 false cross-language edges). The name is now taken from the first long_identifier_or_op under identifier_pattern.

Known limits (deliberate v1 scope)

  • .fsi signature files not wired (language_signature() exists in the grammar; follow-up).
  • Computation-expression internals (async { } / task { } bodies) and #r/#load script directives are not specially handled.
  • Object expressions ({ new IDisposable with ... }) contribute their member bodies' calls but no interface edge.

Checks

  • uv run pytest tests/ -q: 5,315 passed; the only 2 failures (test_labeling.py::test_label_communities_batches_when_over_batch_size, test_watch.py::test_watch_handler_honors_graphifyignore) reproduce on clean v8 without this change.
  • uv lock updated; uv sync --all-extras --frozen clean.
  • CHANGELOG entry added under 0.9.53 (move to the next version as appropriate).

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). Optional [fsharp] extra.

.fs/.fsx join the dotnet interop family (extract/analyze/build maps) so
F#->C# references rewire across the seam instead of dangling.

Follows extractors/ocaml.py: sourceless ref stubs (Graphify-Labs#1402), local-def
ambiguity tracking, two-pass call resolution, qualified-call gating.

Registration is covered by tests/test_fsharp_registration.py, one named
test per wiring point; each entry was mutation-checked (removed in turn,
confirming a test fails). Graded on a real mixed Rust/C#/F# repo: 100
files, 2361 sourced nodes, 10238 call edges, 20/20 sampled call edges
verified at their cited source lines.

Known limits: .fsi signature files not wired (language_signature() is a
follow-up); computation expressions and #r/#load directives deferred.

@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 calleegraphify/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 nodegraphify/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 entirelytests/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 regressionextract_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 regressionemit_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 regressionemit_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 regressionwalk()

fans out to 15 callees (efferent coupling).

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

…test

Both from graphify's own review of this PR:

- emit_member keyed nodes by (file, member) only, so two types in one
  file each having e.g. Dispose merged into a single node. Ids now carry
  the owning type's label; on a real mixed corpus this un-merged 18
  member nodes. New test: two same-file types with a Dispose member must
  yield two nodes.

- test_qualified_external_call_stays_distinct asserted only inside a
  loop over matching edges, so it passed vacuously when no call edge was
  emitted at all. It now first asserts the edge exists.
@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 typesgraphify/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 regressionextract_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 regressionemit_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 regressionemit_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 regressionwalk()

fans out to 15 callees (efferent coupling).

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

…nums

Ten findings from an adversarial review pass, each verified against the
live grammar before fixing:

- Dotted callees (application > dot_expression): `Grasp.Telemetry.init
  args` and method-on-expression calls now record call edges (+381 raw
  call edges on a real 100-file corpus). Pipe operands accept them too.
- `let rec f ... and g ...`: every and-joined head is minted and body
  calls attribute to their own binding (no false f->f self-loop, no
  vanished g).
- Namespaces are canonical: `namespace Grasp.Core` emits the same
  csharp_namespace:<sha1> node id as the C# path, with type: "namespace",
  so N files merge into one hub, the canonicalize pass applies, and the
  unique-stub rewire skips them. Namespace segments no longer whitelist
  local qualified-call binding (corpus-wide names must stay stubs).
- Members: labels use the dotnet `.Name()` method convention so the
  rewire treats them as methods, not type-like unique-stub targets.
- Union cases are type-scoped (two same-file `Ok` cases stay distinct)
  and the 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.
- A comment between pipe operand and operator no longer kills the edge.
- Destructuring `let (a, b) = ...` mints one definition per bound name.
- `.fs` dispatch sniffs for GLSL fragment shaders (mirrors the .m
  ObjC/MATLAB sniff) instead of ERROR-parsing them into dotnet stubs.
- Registration tests no longer module-level-skip the pure-literal
  registry guards on machines without the grammar.

One regression test per finding; 30 F# tests total, all registration
mutations still killed, 20/20 sampled corpus call edges verified at
their cited lines.
@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 regressionextract_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 regressionemit_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 regressionemit_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 regressionmint_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 regressionwalk()

fans out to 15 callees (efferent coupling).

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

Nine distinct defects from a four-arm adversarial panel (Codex + three
Claude models), every finding independently reproduced against the live
grammar before fixing:

- Generic type names: `type Box<'T>` minted a node labelled "T" (the
  last type parameter) — and a `when 'T :> IDisposable` constraint
  minted a sourced IDisposable, reopening the round-2 hazard on a new
  path. The type's name now comes from type_name's long_identifier child
  only.
- `type X with` (type_extension) no longer mints a SOURCED node for the
  possibly-foreign type it augments (an extension of System.String could
  absorb every String reference in the corpus rewire; reproduced
  end-to-end with a C# consumer). Members attach to a sourceless stub.
- Heritage edges: `inherit Base()` -> inherits, `interface I with` ->
  implements (INFERRED, to stubs) — previously nothing was emitted, so
  the rewire's supertype guard could never protect F# base types.
- Object-expression members ({ new IFoo with ... }) are anonymous: no
  minted member, body calls attribute to the enclosing binding, and
  local_defs is no longer poisoned into false ambiguity with real
  same-named bindings.
- Active patterns ((|Even|Odd|)) and operator definitions ((+.)) mint
  nodes with their delimited spelling; body calls attribute to them, not
  the enclosing module.
- `member val` auto-properties are emitted (property_or_ident sits
  directly under member_defn, no method_or_prop_defn wrapper).
- Definition ids 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 stay distinct nodes.
- Function labels use the engine `name()` convention, excluding F#
  functions from `_is_type_like_definition`'s unique-stub TYPE rewire
  (a Python `parse()` stub could previously bind to an F# `parse`);
  `_node_label_key` strips punctuation so cross-file matching holds.
- `open A.B.C` 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 any class named
  `Text` (reproduced).
- GLSL sniff hardened (void main(, vec2/3/4, fragColor, sampler2D; bare
  `//` dropped from the F# markers) — a modern no-#version shader parsed
  as F#; and the guard test is now mutation-killable (its shader carries
  an F# marker inside a comment, so deleting the GLSL check flips it).

40 F# tests, all registration mutations killed, 20/20 sampled corpus
call edges verified at their cited lines; heritage edges now appear on
the real corpus (5 inherits / 3 implements from F# sources).
@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 withimplements (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 4KBgraphify/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 markersgraphify/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 assertionstests/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 regressionextract_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 regressionemit_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 regressionemit_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 regressionmint_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 regressionwalk()

fans out to 16 callees (efferent coupling).

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

…ractor fixes

Round 4 ran a redesigned process: a mechanical 58-construct coverage
oracle built FIRST (tests/fsharp_coverage_oracle.py — presence AND
commission expectations, forbidden-label sets, self-loop checks), a
frozen corpus baseline for differential reconciliation, five review arms
with differentiated lenses (corpus-integration, end-to-end pipeline,
test-adequacy/mutation, grammar-coverage, plain-brief control), a
cross-examination pass on contested fix shapes, and fixer!=verifier
confirmation of each fix against its original repro.

13 verified findings folded:

- GLSL/F# sniff redesigned as evidence-tiered, comment-stripped,
  line-anchored, 64KB window: the round-3 version DROPPED real F# whose
  comments mention "uniform distribution"; the first redesign was killed
  in cross-examination by a real corpus line starting `float count /` —
  weak type-keyword evidence no longer overrides an F# declaration.
- .NET imports repoint now runs for pure-F# corpora (the resolver is
  language-agnostic but was gated on .cs presence; every F#-only repo
  silently lost its open edges in build's dangling prune). The
  C#-shaped TYPE-reference resolver stays gated: F# nodes do not yet
  carry its metadata contract — documented follow-up, which is also
  what cross-language constructor-call resolution needs.
- Object-expression `references` edge was dead code (wrong child type;
  zero hits on 11 corpus object expressions) — now fires, with a
  positive test.
- Generic heritage: `inherit Base<'T>()` / `interface IComparer<'T>`
  wrap in generic_type and were silently skipped; the shared
  type-reference reader now applies the same name-child rule as the
  round-3 type_name fix (2 live corpus recoveries).
- `static let` bindings in classes mint and resolve (previously
  invisible; member calls to them dangled on rewireable stubs).
- Class-internal `let capacity` no longer case-folds onto member
  `.Capacity()` (member ids carry a kind tag) — the real corpus had
  lost RingBuffer's public member and gained a false self-loop.
- Composition operators `>>`/`<<` record both operands as callees
  (deliberately not generalized to custom operators: bind vs Kleisli
  differ on which operand is data).
- `inherit Base(mkArg ())` constructor-argument calls are walked.
- Partial active patterns keep their spelling: `(|Int|_|)`, not `(|Int|)`.
- The round-2 annotated-let regression test was proven vacuous by
  mutation (its source parses down the other branch); a single-arg
  variant now exercises bound_value_names, whose naive reversion
  changed 546 corpus edges while all tests stayed green.
- Positive coverage for local_containers qualified-call binding and
  abstract-member emission (both features previously deletable with
  every test green), heritage-confidence pin, comment-only-marker
  sniff regression test.
- Dead code removed: operator register_def (nothing resolves by
  operator spelling), unreachable type_extension in _TYPE_DEFN_KINDS.

56 F# tests + 58/58 oracle constructs; all registration mutations
killed; full suite 5,354 passed / 0 failed; corpus differential vs the
frozen baseline reconciled row-for-row (+27/-9: generic implements
recoveries, references edges appearing, collision un-merges, partial-AP
relabels); 20/20 sampled call edges verified at cited lines.
@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 Pythontests/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 SyntaxErrortests/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 heuristicgraphify/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 directorytests/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 missingtests/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 regressionextract_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 regressionemit_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 regressionemit_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 regressionmint_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 regressionwalk()

fans out to 17 callees (efferent coupling).

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

Three of five advisory findings verified real (two HIGHs refuted with
compile evidence — see PR thread):

- The coverage oracle used a machine-local temp dir (dir="/data/tmp");
  now the system default, so it runs anywhere including upstream CI.
- The .fs sniffer strips a leading UTF-8 BOM before line-anchoring —
  Windows-authored F# commonly leads with one, and a single-marker-line
  file would otherwise miss its only strong F# marker. Regression test.
- test_missing_grammar_is_reported_not_raised moved out of the
  importorskip'd module: it FAKES the ImportError, so it must run
  precisely on machines where the grammar is actually absent.
@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.py58/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 pathstests/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 64KBgraphify/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 onlygraphify/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 problemtests/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 testtests/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 regressionextract_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 regressionemit_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 regressionemit_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 regressionmint_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 regressionwalk()

fans out to 17 callees (efferent coupling).

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

Bot round-6 find (the automated review's first probe-level catch missed
by all four panel arms): `B.helper` where module B defines no helper
bound EXTRACTED to A's helper — the qualifier gate only checked that the
qualifier was SOME local container, never that the callee belonged to
that container.

register_def now has a per-container sibling index: qualified calls bind
EXTRACTED only via the qualifier's own members; a local qualifier that
does not own the name yields a full-path INFERRED stub (rewireable, but
never a false local edge). A container label claimed twice in one file
is ambiguous and binds nothing.

Corpus effect: 85 receiver-qualified calls (d.U32, capture.Snapshot, ...)
demoted from false EXTRACTED local binds to honest full-path stubs, and
container-scoped positives added correct binds (+54 net calls). 58 F#
tests + 58/58 oracle + all mutants killed + full suite 5,356/0.
@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 NameErrorrun_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# extractorgraphify/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 limitgraphify/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 emittedgraphify/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 typegraphify/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 NameErrortests/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 regressionextract_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 regressionemit_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 regressionemit_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 regressionmint_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 regressionwalk()

fans out to 18 callees (efferent coupling).

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

Bot round-7: one real micro-issue taken (the sniffer read the whole file
before slicing to 64KB — now a bounded read); nested-destructuring
find_all behavior pinned with a test and an intent comment (the grammar
nests inner patterns beside identifier_pattern, never inside one, so
stop-at-match is correct — probed live). Interface-implementation
emission re-verified on the record shape (edge present); .fsi remains a
declared deferral; the oracle NameError report is a repeat of the
round-6 refutation (file runs 58/58, exit 0).
@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.py58/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 unparsablegraphify/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 namegraphify/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 prefixgraphify/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 problemtests/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# testtests/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 regressionextract_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 regressionemit_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 regressionemit_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 regressionmint_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 regressionwalk()

fans out to 18 callees (efferent coupling).

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

Bot round-8 find, probe-confirmed: a Forth .fs file with a line starting
'(*' dispatched to the F# extractor. The marker was for block-comment
license headers, but any compilable F# file surfaces a real declaration
line within the 64KB window anyway. Regression tests both ways (Forth
rejected; block-comment-headed F# still dispatched).
@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 incorrectlygraphify/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 partsgraphify/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 emittedgraphify/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 checkstests/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 failurestests/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 regressionextract_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 regressionemit_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 regressionemit_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 regressionmint_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 regressionwalk()

fans out to 18 callees (efferent coupling).

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

Bot round-9 find, probe-confirmed: 'uniform ' as STRONG GLSL evidence
dropped a real F# file whose continuation line calls a function named
uniform. Strong tier now holds only shapes impossible as F# line-starts
(directives, typed io decls like 'in vec3', void main, gl_); uniform/
varying/precision join the weak tier, which never overrides a strong F#
declaration. Regression tests both ways; all prior shader cases still
reject; 0 of 100 corpus .fs files affected either way.
@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.py58/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 partsgraphify/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 GLSLgraphify/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 problemtests/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 regressionextract_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 regressionemit_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 regressionemit_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 regressionmint_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 regressionwalk()

fans out to 18 callees (efferent coupling).

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

…dentifier

Bot round-10, probe-confirmed: F# calling a function named layout on a
continuation line dropped the whole file. Applies the round-9 principle
uniformly: only shapes impossible as F# line-starts stay strong. Shader
cases hold via 'void main'/weak accumulation; 0 corpus files affected.
@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 namegraphify/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# filesgraphify/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 datagraphify/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 problemtests/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 testtests/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 regressionextract_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 regressionemit_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 regressionemit_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 regressionmint_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 regressionwalk()

fans out to 18 callees (efferent coupling).

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

…s directives only

Closes the marker class that bot rounds 8-10 had been walking one word per
round ((* / uniform / layout): a proactive audit probed every remaining
strong marker against tree-sitter-fsharp itself for line-start validity.

Result: gl_ (interop identifiers: `gl_makeCurrent ctx`), `in vecN` (verbose
`let x = 1.0` / `in vec2 x x`), `out vecN` (`out` is a plain identifier),
and `void main` (`let void = 1` parses -- void is not a lexer-level F#
keyword) are ALL grammar-valid F# line-starts, so any of them as strong
evidence rejects a real F# file whole. Only #version/#extension are
grammar-invalid at an F# line start; the strong tier is now exactly those
two directives, and every word-shaped marker lives in the weak tier, which
never overrides a strong F# declaration.

Also re-anchors test_glsl_guard_is_load_bearing_for_marker_collisions on
'#version' -- its previous shader would now reject via the weak/default path,
so the delete-strong-tier mutant needed a new carrier.

Gates: 70 F# tests (5 new: one acceptance per demoted family, killing its
re-promotion mutant, plus a headerless gl_ shader rejection), oracle 58/58,
full suite green, corpus reclassification 99/99 .fs files unchanged (0
flips -- no extractor-output change), all 5 targeted mutants killed
(4 re-promotions + strong-tier deletion).
@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/4out is not an F# keyword; out vec3 v is a plain application;
  • void mainvoid 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 indentgraphify/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 namegraphify/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 contractgraphify/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 problemtests/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 assertiontests/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 regressionextract_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 regressionemit_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 regressionemit_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 regressionmint_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 regressionwalk()

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

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