Skip to main content

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}