Skip to main content

devscout_rs/
graph.rs

1// Artifact build/load, fragments cache, incremental reuse.
2// Writes are atomic (tmp+rename); fragments cache keying is load-bearing --
3// a mismatch makes reuse break silently.
4//
5// This module owns every serde struct for graph.json + the fragments-cache
6// pair (fragments-v21.json, fragments-index-v21.json), plus their path resolution,
7// atomic I/O, and the cache-then-resolve-then-write orchestration
8// (`rebuild_graph`). The pure resolution ladder that
9// turns fragments into `defs`/`edges` lives in `resolve.rs` and returns the
10// `Graph` value this module serializes -- see that module for the ladder
11// itself.
12//
13// Schema notes (the on-disk format graph.json must keep):
14//   - graph.json is compact JSON with NO indentation:
15//     `{"key":value,...}`, no spaces. `serde_json::to_vec` (non-pretty)
16//     matches this by default.
17//   - Key order is insertion order, not sorted. Defs keep first-insertion
18//     order (partial-class duplicates land in
19//     `also_in`, not a second top-level entry) -- backed here by a
20//     Vec<Def> + HashMap<id, index> pair (`build_def_index` in resolve.rs)
21//     rather than a HashMap alone, which has no defined iteration order.
22//   - `also_in` is omitted entirely (not `[]`) when a def has no additional
23//     declaring sites -- `#[serde(skip_serializing_if = "Vec::is_empty")]`.
24//   - `edges` are shape-heterogeneous by `kind`: inherits/uses-type/
25//     uses-member share `{kind, from_file, from_line, to, to_file}`;
26//     imports is `{kind, from_file, from_line, target}`; ambiguous is
27//     `{kind, origin, from_file, from_line, raw, candidates,
28//     candidate_count}`. Modeled as an internally-tagged enum
29//     (`#[serde(tag = "kind")]`) -- serde always emits the tag field first,
30//     matching every one of these shapes' field order.
31//   - a `uses-member` edge appends three more keys AFTER that shared prefix,
32//     in this exact order: `heuristic` (omitted when precise), `tier`, then
33//     `member`. `tier` names WHICH guess tier emitted the row and is present
34//     exactly when `heuristic` is; `member` names the member the reference
35//     reads or calls and is written on EVERY uses-member edge, the precise
36//     ones included -- it is the one fact the row never carried. Both are
37//     omit-when-`None`, so the append-last rule every other added key follows
38//     holds here too; the other two flagged kinds (inherits, uses-type) stay
39//     exactly as they were.
40//   - `schema_version` is `GRAPH_SCHEMA_VERSION`, which the `tier`/`member`
41//     append above moved to 2, and which the `implements`/`overrides` edge
42//     kinds moved to 3. `rebuild_graph`'s unchanged fast path reads
43//     the first bytes of an existing graph.json and refuses to reuse one
44//     written at an older version, so a stale artifact is rebuilt on the next
45//     `map` even when not one fragment moved.
46//   - `units` is the LAST key of the whole object, appended after `names`
47//     and omitted entirely (not `[]`) when the repo declares no `.csproj` --
48//     so a tree without one serializes byte-for-byte as it did before the
49//     project model existed. Each row's own key order is fixed too: `id`,
50//     `name`, then `refs` (omitted when the project references nothing) and
51//     `test` (omitted when false). A unit's DIRECTORY is not persisted: it
52//     is `id`'s parent, recomputed on read (`project::units_from_graph`),
53//     and neither is per-file/per-def membership -- that is derived from the
54//     unit list by `ProjectModel` rather than stored.
55//   - `stats.edges_by_kind` has a FIXED key order (inherits, uses-type,
56//     imports, uses-member, ctor-di, implements, overrides) -- not
57//     alphabetical, not insertion order of first edge seen. A plain struct
58//     with that declared field order reproduces it.
59//   - `stats.ambiguous_pct` is `round(x*1000)/10`: a number that prints
60//     WITHOUT a decimal point when whole (`20`, not `20.0`) and with
61//     exactly one decimal digit otherwise (`33.3`). Rust's f64 Serialize
62//     (ryu-backed) does not reproduce this -- see `Percent1` below, which
63//     stores tenths as an integer and branches serialize_i64/serialize_f64
64//     so whole values come out as bare integers, without needing serde_json's
65//     `arbitrary_precision`/`raw_value` features (neither is enabled).
66//   - fragments.json / fragments-index.json are objects keyed by
67//     repo-relative path, in file-walk order (fragments-index.json is
68//     rebuilt by iterating the same cache object fragments.json used).
69//     A `HashMap` has no serialization order guarantee and `serde_json`'s
70//     default `Map` (no `preserve_order` feature, which is off here) sorts
71//     by key -- neither matches. `OrderedMap<V>` below hand-rolls
72//     Serialize/Deserialize (using serde's own `SerializeMap` / `MapAccess`
73//     traits, not a bespoke JSON writer) to preserve insertion order.
74//   - `built_at_head` records `git -C <root> rev-parse HEAD` (`null` on any
75//     failure incl. no commits yet). `manifest.rs` already owns that
76//     (`manifest::git_head`) -- `resolve_graph` (resolve.rs) calls THAT, so
77//     this module does not duplicate it.
78
79mod artifact;
80mod cache;
81mod compiler_facts;
82mod def;
83mod edge;
84mod fragment;
85mod fragment_types;
86mod imports;
87mod ordered;
88mod paths;
89mod rebuild;
90
91pub use artifact::{read_graph, Graph, GraphName, GraphUnit, Stats, GRAPH_SCHEMA_VERSION};
92pub use cache::{index_is_stale, read_fragments_index, FragmentCacheEntry};
93pub use compiler_facts::{
94    admit, admit_and_publish, compiler_facts_json_path, expectations_for, locate_engine,
95    read_compiler_facts, run_engine, AdmissionExpectations, AdmittedFacts, Coverage,
96    IncompleteUnit, Profile, PublishError, RefusalReason, COMPILER_FACTS_ARTIFACT_SCHEMA_VERSION,
97    COMPILER_FACTS_CONTRACT_VERSION, COMPILER_FACTS_FORMAT, DEFAULT_OUTPUT_CAP, DEFAULT_TIMEOUT,
98    EXPECTED_CONTEXT_SCHEMA_VERSION, EXPECTED_DEPENDENCY_FINGERPRINT, EXPECTED_ENGINE_REVISION,
99    EXPECTED_PRODUCER_NAME, SCOUT_COMPILER_ENGINE,
100};
101#[cfg(test)]
102pub(crate) use compiler_facts::{recompute_context_summary, CompilationRef};
103pub use def::{AlsoIn, Def};
104pub use edge::{Candidate, Edge, EdgesByKind, HeuristicByTier, HeuristicTier};
105pub use fragment::{fragment_from_extraction, markup_fragment};
106pub use fragment_types::{
107    AnyFragment, FragDef, FragExtensionMethod, FragFact, FragLambdaSlot, FragName, FragRef,
108    FragRegistration, FragUsing, Fragment,
109};
110pub use imports::{
111    imported_edges_json_path, parse_imported_edges, read_imported_edges, write_imported_edges,
112    EdgeEnd, ImportRecord, ImportedEdges, Provenance, IMPORTED_EDGES_FORMAT,
113    IMPORTED_EDGES_SCHEMA_VERSION, IMPORTED_EDGE_KINDS,
114};
115pub use ordered::{OrderedMap, Percent1};
116pub use paths::{graph_json_path, project_units_path};
117pub use rebuild::{rebuild_graph, GraphFile, RebuildOutcome};
118
119#[cfg(test)]
120mod tests;