Logo TestPrune

A symbol has one node and one source occurrence per declaring file

Supersedes ADR 0002.

A public F# declaration in a signature file (.fsi) and its implementation have the same compiler identity. The signature is not code of its own. It describes the implementation. ADR 0002 still gave it a second graph node under the reserved namespace TestPrune.__Signature__., joined to the canonical symbol by a bridging edge. The only reason was storage: a symbols row held one source_file, line range and content_hash, re-indexing a file deleted rows by source_file, and so the last file indexed overwrote the other's hash. That split cost a string-prefix convention the compiler cannot check. Every consumer that enumerates symbols (dead code, reports, anything new) had to know to skip the prefix.

The store now models the declaration directly. A symbols row is the node: its name, kind, containment parent and whether it is only an extern placeholder. Each file that declares it contributes a symbol_occurrences row with that file's location and content hash. Both files analyze to the same canonical names. The signature's occurrence changes when the signature changes, so its consumers are selected by the ordinary edges to that name, without any bridge.

Per-file facts are owned by the file whose analysis produced them, not by the symbol they hang off: dependency edges, test methods and attributes each carry a source_file. AstAnalyzer.factOwner states the rule once. A fact belongs to its anchor symbol's occurrence in the same analysis result, or, when the anchor is declared elsewhere, to the result's single file. A shared-literal bridge is anchored at its producer. Both the SQLite store and the in-memory store use this rule. Re-indexing Library.fs therefore replaces only Library.fs's occurrences and edges. Library.fsi's occurrence and the edges it contributes survive, for example the types named in a val signature. A symbol row survives while any occurrence remains. A real symbol with no occurrence left was removed from the code, so it is deleted and its incoming edges go with it, as before. Coverage points belong to an occurrence, because their offsets are measured from that occurrence's lines.

GetSymbolsInFile and GetAllSymbols return occurrences, so a symbol declared in a signature appears once per declaring file. Dead-code reporting shows one occurrence per name and prefers the implementation, since that is the code to delete.

ADR 0002 rejected an occurrences table because it "would require a schema migration". The index is a cache: a SchemaVersion bump (v14) deletes and rebuilds it. The real cost was code, meaning source lookup, orphan removal and fact ownership in both stores, and that code is the change recorded here.

Fact ownership widens each edge row with its source_file. That would slow the reverse walk in QueryAffectedTests, so idx_deps_to now covers (to_symbol_id, from_symbol_id) and the walk reads no table rows. On a synthetic graph (50,000 symbols, about 200,000 edges, 5,000 tests), measured in process CPU time against the v13 store over four alternating rounds, 30 selection queries chose the identical 88,664 tests in about 40% less time. A single-file re-index cost the same, within noise. A full rebuild's SQLite writes cost about 37% more (7.4 s to 10.1 s), because each declaration now writes a symbol row and an occurrence row.

ADR 0002's regressions pass unchanged against this model: changed, removed and unchanged declarations, full indexing, implementation-only and signature-only replacement, and reversed file order. SymbolOccurrenceStoreTests pins the store seam with hand-built results. Re-indexing either file ALONE keeps the other file's occurrence, edges and consumer selection; this is the 8.2.0 under-selection, tested in both directions. SQLite and the in-memory store agree on every port read whichever file was re-indexed last. A v13 database is recreated as v14 through the schema-version path. SignatureOccurrenceTests pins the analysis seam: both files emit the canonical names, each with its own hash, with no synthetic namespace, and a file without a signature keeps its ordinary graph.

Type something to start searching.