devscout_rs/graph/artifact.rs
1use std::fs;
2use std::io;
3use std::path::Path;
4
5use serde::{Deserialize, Serialize};
6
7use super::def::Def;
8use super::edge::{Edge, EdgesByKind, HeuristicByTier, SemanticStats};
9use super::ordered::Percent1;
10use super::paths::{atomic_write_json, graph_json_path};
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13/// Represents `Stats`.
14pub struct Stats {
15 /// The def count value.
16 pub def_count: usize,
17 /// The file count value.
18 pub file_count: usize,
19 /// The edges by kind value.
20 pub edges_by_kind: EdgesByKind,
21 /// The ambiguous count value.
22 pub ambiguous_count: usize,
23 /// The ambiguous pct value.
24 pub ambiguous_pct: Percent1,
25 /// The unresolved external count value.
26 pub unresolved_external_count: usize,
27 /// Appended LAST, and always serialized (unlike the edge flag above): the
28 /// key is written unconditionally, so a graph with no guesses still
29 /// carries `"heuristic_edge_count":0`. `default` is for the READ side only
30 /// -- a graph.json written before this counter existed has no such key,
31 /// and it must read back as 0 rather than fail to parse.
32 #[serde(default)]
33 pub heuristic_edge_count: usize,
34 /// Test-coverage stage -- appended LAST and always serialized, like the
35 /// stage-4 counter above it. Counts merged DEF ROWS, not fragment entries,
36 /// so a partial test class split across two files is one test def.
37 #[serde(default)]
38 pub test_def_count: usize,
39 /// The TS resolver's own four counters, omitted entirely when the repo
40 /// carries no TS fragment (the same omit-when-empty rule every other
41 /// appended fact follows). Appended after `test_def_count`, and NOT last
42 /// any more: `heuristic_by_tier` below is the newer fact and takes the
43 /// tail, so a TS repo's stats block appends in the order the facts were
44 /// added rather than interleaving the newest one before an older key.
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub ts: Option<crate::tsgraph::TsStats>,
47 /// The tier split of `heuristic_edge_count`, appended LAST and always
48 /// serialized, like the two counters above it. `default` is for the READ
49 /// side only: a graph.json written before the tiers existed has no such
50 /// key and must read back as two zeros rather than fail to parse.
51 ///
52 /// A C#-only repo writes no `ts` key at all, so for such a tree this key
53 /// still follows `test_def_count` directly and the bytes are unchanged.
54 #[serde(default)]
55 pub heuristic_by_tier: HeuristicByTier,
56 /// Whether the repository's own handler registrations contributed to the
57 /// message vocabulary the bus pass ran with. Appended LAST and written
58 /// ONLY alongside a bus-hop count, so a graph with no hop carries no such
59 /// key and its bytes are unchanged.
60 ///
61 /// `Some(false)` is the answer worth reading: the pass fell back to the
62 /// handler shapes this engine ships because the repository registers its
63 /// handlers somewhere this pass cannot see them -- by scanning an
64 /// assembly, most often. Hops are still emitted, but their coverage is
65 /// whatever the fallback shapes happened to match, which is a gap to
66 /// state rather than one to infer from a low count.
67 #[serde(
68 default,
69 rename = "bus_vocabulary_derived",
70 skip_serializing_if = "Option::is_none"
71 )]
72 pub bus_vocabulary_derived: Option<bool>,
73 /// The resolve-time compiler-fact consumption path's own run-level
74 /// counters, appended LAST and omitted entirely when the semantic layer
75 /// did not load for this run -- see `SemanticStats`'s own doc comment.
76 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub semantic: Option<SemanticStats>,
78}
79
80/// One row of the full name index. Field order (`name`, `kind`,
81/// `file`, `line`, `owner`) is significant; `owner` carries the
82/// declaring type's def id for a member and is omitted for a type, an enum
83/// member, and every markup or resource key.
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub struct GraphName {
86 /// The name value.
87 pub name: String,
88 /// The kind value.
89 pub kind: String,
90 /// The file value.
91 pub file: String,
92 /// The line value.
93 pub line: usize,
94 #[serde(default, skip_serializing_if = "String::is_empty")]
95 /// The owner value.
96 pub owner: String,
97}
98
99/// One `.csproj` project as graph.json persists it.
100///
101/// Field order (`id`, `name`, `refs`, `test`) is significant, and the last two are
102/// omit-when-empty/omit-when-false: a leaf project that references nothing
103/// and is not a test project serializes as just its `id` and `name`.
104///
105/// Deliberately NOT `project::Unit`: that type also carries `dir`, which is
106/// always `id`'s parent directory and so is recomputed on read rather than
107/// stored (`project::units_from_graph`). Nothing about which FILE belongs to
108/// which unit is persisted either -- `ProjectModel` derives that from the
109/// unit list.
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct GraphUnit {
112 /// Repo-relative path to the `.csproj` file, which is also this unit's
113 /// identity -- what `refs` entries name.
114 pub id: String,
115 /// The project name (the csproj file name without its extension).
116 pub name: String,
117 /// The `id`s of this project's DIRECT `ProjectReference` targets, not
118 /// transitively closed. Omitted when empty.
119 #[serde(default, skip_serializing_if = "Vec::is_empty")]
120 pub refs: Vec<String>,
121 /// Whether this is a test project. Omitted when false.
122 #[serde(default, skip_serializing_if = "is_not_test")]
123 pub test: bool,
124}
125
126fn is_not_test(b: &bool) -> bool {
127 !*b
128}
129
130/// The version stamped into every graph.json this build writes, and the one
131/// `rebuild_graph` demands before it reuses an artifact it did not just
132/// produce.
133///
134/// Bumped to 2 when `uses-member` edges gained `tier` and `member`, and to 3
135/// when the `implements`/`overrides` edge kinds joined the graph: a
136/// schema-2 graph is READABLE (the two new `edges_by_kind` counters default to
137/// 0 and no edge of either new kind can be present) but it is missing facts
138/// the query layer now reports, so it gets rebuilt rather than trusted.
139pub const GRAPH_SCHEMA_VERSION: u32 = 3;
140
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
142/// Represents `Graph`.
143pub struct Graph {
144 /// The schema version value.
145 pub schema_version: u32,
146 /// The built at head value.
147 pub built_at_head: Option<String>,
148 /// The defs value.
149 pub defs: Vec<Def>,
150 /// The edges value.
151 pub edges: Vec<Edge>,
152 /// The stats value.
153 pub stats: Stats,
154 /// Appended LAST, after `stats`, and omitted when empty: the
155 /// house rule for every added field, and what keeps a graph built over a
156 /// set that declares no name byte-identical to what it was.
157 #[serde(default, skip_serializing_if = "Vec::is_empty")]
158 pub names: Vec<GraphName>,
159 /// The repo's `.csproj` projects, sorted by `id` -- appended LAST, after
160 /// `names`, and omitted entirely when the repo declares none. A tree with
161 /// no `.csproj` therefore serializes exactly as it did before the project
162 /// model existed.
163 #[serde(default, skip_serializing_if = "Vec::is_empty")]
164 pub units: Vec<GraphUnit>,
165}
166
167/// Reads and deserializes the repository graph, returning `None` on failure.
168pub fn read_graph(root: &Path) -> Option<Graph> {
169 let text = fs::read_to_string(graph_json_path(root)).ok()?;
170 serde_json::from_str(&text).ok()
171}
172
173pub(crate) fn write_graph(root: &Path, graph: &Graph) -> io::Result<()> {
174 atomic_write_json(&graph_json_path(root), graph)
175}