Skip to main content

spec_spine_types/
registry.rs

1//! Registry DTOs: the spec-as-source view, emitted by `compile` as
2//! `registry.json`. Field names serialize to `camelCase` (the JSON contract),
3//! distinct from the `snake_case` authored [`crate::Frontmatter`] grammar.
4//!
5//! The compiler (Phase 2) populates these from parsed frontmatter plus computed
6//! fields (`spec_path`, `section_headings`, the content hash). Shapes are ported
7//! from OAP `registry.schema.json` (`featureRecord`, `build`, `violation`),
8//! pruned to the generic v1 surface; overlay fields (compliance, factory,
9//! capability/registry/profile) are intentionally absent (see ยง10.4).
10
11use std::collections::BTreeMap;
12
13use serde::{Deserialize, Serialize};
14
15use crate::edges::{
16    CoAuthorityItem, ConstrainItem, ExtendItem, Origin, ReferenceItem, RefineItem, SupersedeItem,
17};
18use crate::frontmatter::{Implementation, Risk, Status};
19use crate::unit::Unit;
20
21/// The compiled registry: `registry.json`.
22#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct Registry {
25    /// `MAJOR.MINOR.PATCH`; see [`crate::version::REGISTRY_SCHEMA_VERSION`].
26    pub spec_version: String,
27    pub build: Build,
28    pub specs: Vec<SpecRecord>,
29    pub validation: ValidationReport,
30}
31
32/// Deterministic build metadata embedded in `registry.json` (no timestamps:
33/// the wall clock lives in the separate, non-deterministic `build-meta.json`).
34#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct Build {
37    pub compiler_id: String,
38    pub compiler_version: String,
39    /// The input root the registry was compiled from, repo-relative (e.g. `.`).
40    pub input_root: String,
41    /// SHA-256 over the normalized, path-sorted spec inputs (64 lowercase hex).
42    pub content_hash: String,
43}
44
45/// One spec's entry in the registry.
46#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct SpecRecord {
49    // --- required ---
50    pub id: String,
51    pub title: String,
52    pub status: Status,
53    pub created: String,
54    pub summary: String,
55    /// Repo-relative path: `specs/NNN-slug/spec.md`.
56    pub spec_path: String,
57
58    // --- optional descriptive ---
59    #[serde(default, skip_serializing_if = "Vec::is_empty")]
60    pub authors: Vec<String>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub owner: Option<String>,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub kind: Option<String>,
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub domain: Option<String>,
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub risk: Option<Risk>,
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub implementation: Option<Implementation>,
71    #[serde(default, skip_serializing_if = "Vec::is_empty")]
72    pub depends_on: Vec<String>,
73    #[serde(default, skip_serializing_if = "Vec::is_empty")]
74    pub code_aliases: Vec<String>,
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub feature_branch: Option<String>,
77    /// Markdown headings discovered in the spec body (anchors for sections).
78    #[serde(default, skip_serializing_if = "Vec::is_empty")]
79    pub section_headings: Vec<String>,
80
81    // --- typed edges (8) ---
82    #[serde(default, skip_serializing_if = "Vec::is_empty")]
83    pub establishes: Vec<Unit>,
84    #[serde(default, skip_serializing_if = "Vec::is_empty")]
85    pub extends: Vec<ExtendItem>,
86    #[serde(default, skip_serializing_if = "Vec::is_empty")]
87    pub refines: Vec<RefineItem>,
88    /// Full supersession serializes as a bare predecessor id; a partial item
89    /// serializes as an object (spec 019).
90    #[serde(default, skip_serializing_if = "Vec::is_empty")]
91    pub supersedes: Vec<SupersedeItem>,
92    #[serde(default, skip_serializing_if = "Vec::is_empty")]
93    pub amends: Vec<String>,
94    #[serde(default, skip_serializing_if = "Vec::is_empty")]
95    pub co_authority: Vec<CoAuthorityItem>,
96    #[serde(default, skip_serializing_if = "Vec::is_empty")]
97    pub constrains: Vec<ConstrainItem>,
98    #[serde(default, skip_serializing_if = "Vec::is_empty")]
99    pub references: Vec<ReferenceItem>,
100
101    // --- lifecycle / amendment ---
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub superseded_by: Option<String>,
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub retirement_rationale: Option<String>,
106    #[serde(default, skip_serializing_if = "Vec::is_empty")]
107    pub amends_sections: Vec<String>,
108    #[serde(default, skip_serializing_if = "Vec::is_empty")]
109    pub unamendable: Vec<String>,
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub amendment_record: Option<String>,
112
113    // --- bootstrap marker ---
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub origin: Option<Origin>,
116
117    // --- overflow ---
118    /// Declared keys carry any JSON value (spec 013); undeclared keys are
119    /// scalars or string arrays.
120    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
121    pub extra_frontmatter: BTreeMap<String, serde_json::Value>,
122}
123
124// ===== sharded committed form (spec 024) =====
125//
126// The committed registry is stored as one file per spec so two PRs that add or
127// edit different specs write disjoint files and never conflict textually on a
128// shared content-hash line. The aggregate [`Registry`] above stays the universal
129// in-memory currency: the compiler projects it to shards, and a reader assembles
130// it back from the shard set. The aggregate `validation` and `build.contentHash`
131// are recomputed on read (cross-spec checks like duplicate-id / dangling edges
132// are pure functions of the assembled record set), never committed.
133
134/// One spec's registry shard: `<derived>/spec-registry/by-spec/<id>.json`.
135/// A PR that adds or edits spec X rewrites only X's shard (spec 024 FR-002).
136#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub struct RegistrySpecShard {
139    /// `specVersion`; see [`crate::version::REGISTRY_SCHEMA_VERSION`].
140    pub spec_version: String,
141    /// SHA-256 over this spec's `spec.md` (the registry's only hashed input,
142    /// matching the pre-shard `build.contentHash` input set). Self-describing
143    /// per-shard staleness.
144    pub shard_hash: String,
145    /// This spec's compiled record.
146    pub record: SpecRecord,
147    /// Validation findings that are a pure function of THIS spec (V-001/002/
148    /// 005/006/007/011/012/013). Cross-spec findings (duplicate id/prefix,
149    /// dangling edges) are recomputed on read from the assembled record set, so
150    /// they are never stored here (storing them would make a sibling spec's PR
151    /// stale this shard). Omitted when empty: a clean spec carries none.
152    #[serde(default, skip_serializing_if = "Vec::is_empty")]
153    pub local_violations: Vec<Violation>,
154}
155
156/// Non-deterministic build metadata sidecar (`build-meta.json`). The wall-clock
157/// `built_at` lives here, never in `registry.json`, and is excluded from every
158/// determinism/golden check. The CLI populates `built_at`; the library never
159/// reads the clock.
160#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct BuildMeta {
163    pub schema_version: String,
164    pub built_at: String,
165    pub compiler_id: String,
166    pub compiler_version: String,
167}
168
169/// Severity tier of a diagnostic.
170#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(rename_all = "lowercase")]
172pub enum Severity {
173    Error,
174    Warning,
175    Info,
176}
177
178/// A single validation/lint/coupling diagnostic.
179#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct Violation {
182    /// A stable code such as `V-001`, `L-003`, `I-004`.
183    pub code: String,
184    pub severity: Severity,
185    pub message: String,
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub path: Option<String>,
188}
189
190/// The registry's validation summary. `passed` is false iff any `error`-tier
191/// violation is present.
192#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct ValidationReport {
195    pub passed: bool,
196    #[serde(default)]
197    pub violations: Vec<Violation>,
198}
199
200impl ValidationReport {
201    /// Build a report from violations, setting `passed` per the error-tier rule.
202    pub fn from_violations(violations: Vec<Violation>) -> Self {
203        let passed = !violations.iter().any(|v| v.severity == Severity::Error);
204        ValidationReport { passed, violations }
205    }
206}