Skip to main content

spec_spine_types/
codebase.rs

1//! Codebase-index DTOs: the code-as-source view, emitted by `spec-spine index`
2//! as `index.json`. Field names serialize to `camelCase`. Shapes are ported from
3//! OAP `codebase-index.schema.json` (3.0.0), pruned to the generic v1 surface and
4//! re-versioned to this library's own schema line (currently `0.3.0`; see
5//! [`crate::version::INDEX_SCHEMA_VERSION`]).
6
7use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::unit::Unit;
12
13/// The compiled codebase index: `index.json`.
14#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct CodebaseIndex {
17    /// `MAJOR.MINOR.PATCH`; see [`crate::version::INDEX_SCHEMA_VERSION`].
18    pub schema_version: String,
19    pub build: IndexBuild,
20    /// Layer 1: the discovered compilation units.
21    pub packages: Vec<PackageRecord>,
22    /// Layer 2: spec ↔ code traceability.
23    pub traceability: Traceability,
24    pub diagnostics: Diagnostics,
25}
26
27/// Deterministic build metadata embedded in `index.json`.
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct IndexBuild {
31    pub indexer_id: String,
32    pub indexer_version: String,
33    pub repo_root: String,
34    /// SHA-256 over the normalized, path-sorted manifest + spec + extra inputs.
35    pub content_hash: String,
36    /// Per-slice content hashes (spec 012): one entry per `[index.slices]`
37    /// key, same normalization as `content_hash`. Absent when no slices are
38    /// configured; loaders tolerate absence (additive MINOR).
39    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
40    pub slice_hashes: BTreeMap<String, String>,
41}
42
43/// The kind of a discovered compilation unit.
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "kebab-case")]
46pub enum PackageKind {
47    RustLib,
48    RustBin,
49    RustLibBin,
50    NpmPackage,
51    NpmWorkspace,
52}
53
54/// A discovered compilation unit (a Rust crate or an npm package).
55#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct PackageRecord {
58    pub name: String,
59    /// Repo-relative POSIX path to the package directory.
60    pub path: String,
61    pub kind: PackageKind,
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub version: Option<String>,
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub edition: Option<String>,
66    /// The owning spec id declared in the manifest's metadata namespace, if any.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub spec_ref: Option<String>,
69}
70
71/// Layer 2: how the corpus maps onto the code, and what is unmapped.
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct Traceability {
75    pub mappings: Vec<TraceMapping>,
76    /// Specs claiming code that resolves to no location.
77    pub orphaned_specs: Vec<String>,
78    /// Package paths with no governing spec.
79    pub untraced_code: Vec<String>,
80}
81
82/// One spec's mapping onto the code.
83#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct TraceMapping {
86    pub spec_id: String,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub spec_status: Option<String>,
89    #[serde(default, skip_serializing_if = "Vec::is_empty")]
90    pub depends_on: Vec<String>,
91    #[serde(default, skip_serializing_if = "Vec::is_empty")]
92    pub amends: Vec<String>,
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub amendment_record: Option<String>,
95    /// Flat path ownership (whole-file granularity).
96    pub implementing_paths: Vec<ImplementingPath>,
97    /// Typed-unit ownership with physical line-spans.
98    #[serde(default, skip_serializing_if = "Vec::is_empty")]
99    pub resolved_units: Vec<ResolvedUnit>,
100}
101
102/// Where a path-level linkage came from.
103#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
104#[serde(rename_all = "kebab-case")]
105pub enum TraceSource {
106    /// A spec's ownership edge (`establishes`/`extends`/…).
107    SpecEdge,
108    /// A manifest `[package.metadata.<ns>].spec` / `"<ns>".spec` key.
109    ManifestMetadata,
110    /// A `// Spec: …` file-root comment header.
111    CommentHeader,
112    /// Two or more sources agree on this path.
113    Multiple,
114}
115
116/// A path claimed by a spec, with its linkage source.
117#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct ImplementingPath {
120    pub path: String,
121    pub source: TraceSource,
122}
123
124/// Which edge field a resolved unit came from.
125#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "snake_case")]
127pub enum SourceField {
128    Establishes,
129    Extends,
130    Refines,
131    Supersedes,
132    Amends,
133    CoAuthority,
134    Constrains,
135    References,
136}
137
138impl SourceField {
139    /// Ownership-bearing? `references` is the only non-owning edge.
140    pub fn is_ownership(self) -> bool {
141        !matches!(self, SourceField::References)
142    }
143}
144
145/// A typed unit resolved to its physical locations.
146#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct ResolvedUnit {
149    pub unit: Unit,
150    pub source_field: SourceField,
151    /// `false` only for `references` units (the gate ignores them).
152    pub ownership: bool,
153    /// Resolved locations (empty when resolution failed → a diagnostic).
154    pub locations: Vec<ResolvedLocation>,
155}
156
157/// A physical location: a file and an optional line-span (absent ⇒ whole file).
158#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct ResolvedLocation {
161    pub file: String,
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub span: Option<LineSpan>,
164}
165
166/// An inclusive, 1-based line span, aligned with `git diff -U0` hunk ranges.
167#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct LineSpan {
170    pub start_line: usize,
171    pub end_line: usize,
172}
173
174impl LineSpan {
175    pub fn new(start_line: usize, end_line: usize) -> Self {
176        LineSpan {
177            start_line,
178            end_line,
179        }
180    }
181}
182
183/// Index diagnostics, split by tier. `I-003`..`I-009` (in `errors`) block `check`.
184#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct Diagnostics {
187    pub warnings: Vec<Diagnostic>,
188    pub errors: Vec<Diagnostic>,
189}
190
191impl Diagnostics {
192    /// No diagnostics of either tier (used to omit an empty block from a shard).
193    pub fn is_empty(&self) -> bool {
194        self.warnings.is_empty() && self.errors.is_empty()
195    }
196}
197
198// ===== sharded committed form (spec 024) =====
199//
200// The committed index is stored as one file per authority unit so two PRs that
201// touch different specs/packages write disjoint files and never conflict
202// textually. The aggregate [`CodebaseIndex`] above stays the universal in-memory
203// currency: the emitter projects it to shards, and a reader assembles it back
204// from the shard set (orphans / untraced code / `build.contentHash` are pure
205// functions of the shards, recomputed on read, never committed).
206
207/// One spec's traceability shard: `<derived>/codebase-index/by-spec/<id>.json`.
208/// A PR confined to spec X's inputs rewrites only X's shard (spec 024 FR-002).
209#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
210#[serde(rename_all = "camelCase")]
211pub struct IndexSpecShard {
212    /// `schemaVersion`; see [`crate::version::INDEX_SCHEMA_VERSION`].
213    pub schema_version: String,
214    /// SHA-256 over this spec's inputs: its `spec.md`, the source files backing
215    /// its resolved symbol/section/module spans, and the global-inputs scalar
216    /// (config + `extra_hashed_inputs`). Self-describing per-shard staleness.
217    pub shard_hash: String,
218    /// This spec's mapping onto the code.
219    pub mapping: TraceMapping,
220    /// Resolver diagnostics scoped to this spec (`I-003`..`I-009` block `check`).
221    /// Omitted when empty, so a clean spec's shard carries no diagnostics block.
222    #[serde(default, skip_serializing_if = "Diagnostics::is_empty")]
223    pub diagnostics: Diagnostics,
224}
225
226/// One package's inventory shard: `<derived>/codebase-index/by-package/<slug>.json`.
227/// A PR confined to a package's manifest rewrites only that package's shard.
228#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct IndexPackageShard {
231    /// `schemaVersion`; see [`crate::version::INDEX_SCHEMA_VERSION`].
232    pub schema_version: String,
233    /// SHA-256 over this package's manifest (governance projection) folded with
234    /// the global-inputs scalar.
235    pub shard_hash: String,
236    /// The discovered compilation unit.
237    pub package: PackageRecord,
238}
239
240/// A single index diagnostic (`I-###`).
241#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct Diagnostic {
244    pub code: String,
245    pub message: String,
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    pub path: Option<String>,
248}