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;