Skip to content

Commit bd0d729

Browse files
authored
feat: salsa integration (Phase 5) + dogfood tracking (#121)
* chore: track batch 2 features as rivet artifacts (FEAT-093 to FEAT-105) 13 features: rowan Phases 1-3, 6 domain schemas, MCP expansion, schema validate, docs refresh, pre-commit hook. 573 total artifacts. * feat(salsa): wire schema-driven rowan parser into salsa DB (Phase 5) parse_artifacts_v2() tracked function uses extract_schema_driven() from the rowan HIR layer. Schema is a transitive salsa dependency — schema changes invalidate all artifact extraction, source changes only re-extract that file. Feature flag 'rowan-yaml' (default on). Debug builds log warnings if old and new parsers produce different artifact IDs. build_store() now takes schema_set parameter for the new code path. All 7 db.store() call sites updated.
1 parent e4f398e commit bd0d729

4 files changed

Lines changed: 239 additions & 19 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Features shipped in v0.3.1 second batch (2026-04-02)
2+
artifacts:
3+
# ── Rowan parser phases ──────────────────────────────────────────────
4+
5+
- id: FEAT-093
6+
type: feature
7+
title: Rowan YAML CST parser (Phase 1)
8+
status: approved
9+
description: >
10+
Lossless, span-preserving YAML parser using rowan. 28 SyntaxKind
11+
variants, hand-written lexer and recursive-descent parser with
12+
indent tracking. Round-trip guarantee for all project YAML files.
13+
links:
14+
- type: satisfies
15+
target: REQ-002
16+
17+
- id: FEAT-094
18+
type: feature
19+
title: HIR extraction from rowan CST (Phase 2)
20+
status: approved
21+
description: >
22+
Walks rowan CST to extract Vec<SpannedArtifact> with byte-accurate
23+
spans for every field. Cross-validated against serde-based parser.
24+
links:
25+
- type: satisfies
26+
target: REQ-002
27+
28+
- id: FEAT-095
29+
type: feature
30+
title: Schema-driven YAML extraction (Phase 3)
31+
status: approved
32+
description: >
33+
Uses yaml-section and shorthand-links schema metadata to parse
34+
section-based YAML formats. One function replaces the 861-line
35+
hardcoded STPA adapter. Handles generic and section formats.
36+
links:
37+
- type: satisfies
38+
target: REQ-002
39+
- type: satisfies
40+
target: REQ-010
41+
42+
# ── Domain schemas ─────────────────────────────────────────────────
43+
44+
- id: FEAT-096
45+
type: feature
46+
title: DO-178C airborne software schema
47+
status: approved
48+
description: >
49+
14 artifact types covering PSAC through SAS with DAL-based
50+
traceability rules for aviation software certification.
51+
links:
52+
- type: satisfies
53+
target: REQ-010
54+
55+
- id: FEAT-097
56+
type: feature
57+
title: EN 50128 railway software safety schema
58+
status: approved
59+
description: >
60+
14 artifact types with SIL-dependent rules, tool qualification,
61+
independent assessment for railway software.
62+
links:
63+
- type: satisfies
64+
target: REQ-010
65+
66+
- id: FEAT-098
67+
type: feature
68+
title: IEC 61508 functional safety schema
69+
status: approved
70+
description: >
71+
15 artifact types covering the full IEC 61508 V-model with
72+
SIL-based traceability and independent assessment rules.
73+
links:
74+
- type: satisfies
75+
target: REQ-010
76+
77+
- id: FEAT-099
78+
type: feature
79+
title: IEC 62304 medical device software schema
80+
status: approved
81+
description: >
82+
13 artifact types with class-conditional verification rules
83+
for medical device software (Class A/B/C).
84+
links:
85+
- type: satisfies
86+
target: REQ-010
87+
88+
- id: FEAT-100
89+
type: feature
90+
title: ISO/PAS 8800 AI safety lifecycle schema
91+
status: approved
92+
description: >
93+
12 artifact types for AI element safety in road vehicles.
94+
Bridges to STPA-AI for ML controller traceability.
95+
links:
96+
- type: satisfies
97+
target: REQ-010
98+
99+
- id: FEAT-101
100+
type: feature
101+
title: ISO 21448 SOTIF schema
102+
status: approved
103+
description: >
104+
8 artifact types covering functional insufficiency hazards,
105+
triggering conditions, and known/unknown unsafe scenarios.
106+
links:
107+
- type: satisfies
108+
target: REQ-010
109+
110+
# ── Other features ─────────────────────────────────────────────────
111+
112+
- id: FEAT-102
113+
type: feature
114+
title: MCP server expanded to 9 tools
115+
status: approved
116+
description: >
117+
Added rivet_get, rivet_coverage, rivet_schema, rivet_embed,
118+
rivet_snapshot_capture, rivet_add to the MCP server with
119+
proper JSON Schema inputSchema definitions.
120+
links:
121+
- type: satisfies
122+
target: REQ-001
123+
124+
- id: FEAT-103
125+
type: feature
126+
title: rivet schema validate command
127+
status: approved
128+
description: >
129+
Validates loaded schemas are well-formed: checks link types
130+
exist, target types exist, traceability rule references valid.
131+
Found 3 real errors in AADL cross-schema references.
132+
links:
133+
- type: satisfies
134+
target: REQ-010
135+
136+
- id: FEAT-104
137+
type: feature
138+
title: Documentation refresh with 7 new topics
139+
status: approved
140+
description: >
141+
New docs: embed-syntax, schemas-overview, schema/eu-ai-act,
142+
schema/safety-case, schema/stpa-ai, schema/stpa-sec,
143+
schema/research. Updated CLI and YAML references.
144+
links:
145+
- type: satisfies
146+
target: REQ-001
147+
148+
- id: FEAT-105
149+
type: feature
150+
title: Pre-commit hook for formatting and clippy
151+
status: approved
152+
description: >
153+
scripts/pre-commit runs cargo fmt and clippy before every commit.
154+
Prevents CI format failures.
155+
links:
156+
- type: satisfies
157+
target: REQ-001

rivet-cli/src/main.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6540,7 +6540,7 @@ fn cmd_lsp(cli: &Cli) -> Result<bool> {
65406540
};
65416541

65426542
// Build supplementary state for rendering
6543-
let store = db.store(source_set);
6543+
let store = db.store(source_set, schema_set);
65446544
let render_schema = db.schema(schema_set);
65456545
let mut render_graph = rivet_core::links::LinkGraph::build(&store, &render_schema);
65466546

@@ -6617,7 +6617,7 @@ fn cmd_lsp(cli: &Cli) -> Result<bool> {
66176617
match method {
66186618
"textDocument/hover" => {
66196619
let params: HoverParams = serde_json::from_value(req.params.clone())?;
6620-
let store = db.store(source_set);
6620+
let store = db.store(source_set, schema_set);
66216621
let result = lsp_hover(&params, &store);
66226622
connection.sender.send(Message::Response(Response {
66236623
id: req.id,
@@ -6628,7 +6628,7 @@ fn cmd_lsp(cli: &Cli) -> Result<bool> {
66286628
"textDocument/definition" => {
66296629
let params: GotoDefinitionParams =
66306630
serde_json::from_value(req.params.clone())?;
6631-
let store = db.store(source_set);
6631+
let store = db.store(source_set, schema_set);
66326632
let result = lsp_goto_definition(&params, &store);
66336633
connection.sender.send(Message::Response(Response {
66346634
id: req.id,
@@ -6638,7 +6638,7 @@ fn cmd_lsp(cli: &Cli) -> Result<bool> {
66386638
}
66396639
"textDocument/completion" => {
66406640
let params: CompletionParams = serde_json::from_value(req.params.clone())?;
6641-
let store = db.store(source_set);
6641+
let store = db.store(source_set, schema_set);
66426642
let schema = db.schema(schema_set);
66436643
let result = lsp_completion(&params, &store, &schema);
66446644
connection.sender.send(Message::Response(Response {
@@ -6923,7 +6923,7 @@ fn cmd_lsp(cli: &Cli) -> Result<bool> {
69236923
// and append document [[ID]] reference validation so
69246924
// broken wiki-links in markdown files are reported.
69256925
let mut new_diagnostics = db.diagnostics(source_set, schema_set);
6926-
let new_store = db.store(source_set);
6926+
let new_store = db.store(source_set, schema_set);
69276927
new_diagnostics
69286928
.extend(validate::validate_documents(&doc_store, &new_store));
69296929
lsp_publish_salsa_diagnostics(
@@ -6939,7 +6939,7 @@ fn cmd_lsp(cli: &Cli) -> Result<bool> {
69396939
);
69406940

69416941
// Rebuild render state
6942-
render_store = db.store(source_set);
6942+
render_store = db.store(source_set, schema_set);
69436943
let render_schema = db.schema(schema_set);
69446944
render_graph = rivet_core::links::LinkGraph::build(
69456945
&render_store,
@@ -6986,7 +6986,7 @@ fn cmd_lsp(cli: &Cli) -> Result<bool> {
69866986
// including document [[ID]] reference validation.
69876987
let mut diagnostics =
69886988
db.diagnostics(source_set, schema_set);
6989-
let store = db.store(source_set);
6989+
let store = db.store(source_set, schema_set);
69906990
diagnostics.extend(validate::validate_documents(
69916991
&doc_store, &store,
69926992
));

rivet-core/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ rust-version.workspace = true
99

1010

1111
[features]
12-
default = ["aadl"]
12+
default = ["aadl", "rowan-yaml"]
13+
rowan-yaml = []
1314
oslc = ["dep:reqwest", "dep:urlencoding"]
1415
wasm = ["dep:wasmtime", "dep:wasmtime-wasi"]
1516
aadl = ["dep:spar-hir", "dep:spar-analysis"]

rivet-core/src/db.rs

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use crate::model::Artifact;
2121
use crate::schema::{Schema, SchemaFile};
2222
use crate::store::Store;
2323
use crate::validate::Diagnostic;
24+
#[cfg(feature = "rowan-yaml")]
25+
use crate::yaml_hir;
2426

2527
// ── Salsa inputs ────────────────────────────────────────────────────────
2628

@@ -109,6 +111,30 @@ pub fn parse_artifacts(db: &dyn salsa::Database, source: SourceFile) -> Vec<Arti
109111
}
110112
}
111113

114+
/// Parse artifacts from a single source file using the schema-driven rowan parser.
115+
///
116+
/// Uses `yaml_hir::extract_schema_driven` which reads `yaml-section` metadata
117+
/// from the schema to discover sections and auto-convert shorthand links.
118+
///
119+
/// This is a salsa tracked function — results are memoized and only
120+
/// recomputed when the `SourceFile` content or `SchemaInputSet` changes.
121+
#[cfg(feature = "rowan-yaml")]
122+
#[salsa::tracked]
123+
pub fn parse_artifacts_v2(
124+
db: &dyn salsa::Database,
125+
source: SourceFile,
126+
schema_set: SchemaInputSet,
127+
) -> Vec<Artifact> {
128+
let content = source.content(db);
129+
let path = source.path(db);
130+
let source_path = std::path::Path::new(&path);
131+
132+
let schema = build_schema(db, schema_set);
133+
let parsed = yaml_hir::extract_schema_driven(&content, &schema, Some(source_path));
134+
135+
parsed.artifacts.into_iter().map(|sa| sa.artifact).collect()
136+
}
137+
112138
/// Collect parse errors from all source files as diagnostics.
113139
///
114140
/// Each file that fails to parse produces a `yaml-parse-error` diagnostic
@@ -234,9 +260,9 @@ pub fn validate_all(
234260
///
235261
/// Both `validate_all` and this function call `build_pipeline`, which is a
236262
/// plain (non-tracked) helper. The tracked functions that `build_pipeline`
237-
/// delegates to (`parse_artifacts`) are individually cached by salsa, so
238-
/// the repeated calls do NOT re-parse source files — only the lightweight
239-
/// store/schema assembly runs twice.
263+
/// delegates to (`parse_artifacts` / `parse_artifacts_v2`) are individually
264+
/// cached by salsa, so the repeated calls do NOT re-parse source files —
265+
/// only the lightweight store/schema assembly runs twice.
240266
#[salsa::tracked]
241267
pub fn evaluate_conditional_rules(
242268
db: &dyn salsa::Database,
@@ -280,18 +306,53 @@ fn build_pipeline(
280306
source_set: SourceFileSet,
281307
schema_set: SchemaInputSet,
282308
) -> (Store, Schema, LinkGraph) {
283-
let store = build_store(db, source_set);
309+
let store = build_store(db, source_set, schema_set);
284310
let schema = build_schema(db, schema_set);
285311
let graph = LinkGraph::build(&store, &schema);
286312
(store, schema, graph)
287313
}
288314

289315
/// Build an artifact `Store` from all source file inputs.
290-
fn build_store(db: &dyn salsa::Database, source_set: SourceFileSet) -> Store {
316+
///
317+
/// When the `rowan-yaml` feature is enabled, uses the schema-driven rowan
318+
/// parser (`parse_artifacts_v2`) which reads `yaml-section` metadata from
319+
/// the schema. In debug builds, both parsers run and their output is
320+
/// compared as a cross-check.
321+
fn build_store(
322+
db: &dyn salsa::Database,
323+
source_set: SourceFileSet,
324+
schema_set: SchemaInputSet,
325+
) -> Store {
326+
#[cfg(not(feature = "rowan-yaml"))]
327+
let _ = schema_set;
328+
291329
let sources = source_set.files(db);
292330
let mut store = Store::new();
293331
for source in sources {
294-
for artifact in parse_artifacts(db, source) {
332+
#[cfg(feature = "rowan-yaml")]
333+
let artifacts = {
334+
let new_arts = parse_artifacts_v2(db, source, schema_set);
335+
336+
#[cfg(debug_assertions)]
337+
{
338+
let old_arts = parse_artifacts(db, source);
339+
let new_ids: Vec<&str> = new_arts.iter().map(|a| a.id.as_str()).collect();
340+
let old_ids: Vec<&str> = old_arts.iter().map(|a| a.id.as_str()).collect();
341+
if old_ids != new_ids {
342+
log::warn!(
343+
"parser mismatch for {}: old={old_ids:?} new={new_ids:?}",
344+
source.path(db),
345+
);
346+
}
347+
}
348+
349+
new_arts
350+
};
351+
352+
#[cfg(not(feature = "rowan-yaml"))]
353+
let artifacts = parse_artifacts(db, source);
354+
355+
for artifact in artifacts {
295356
// Use upsert to avoid panics on duplicate IDs across files.
296357
store.upsert(artifact);
297358
}
@@ -378,9 +439,9 @@ impl RivetDatabase {
378439
false
379440
}
380441

381-
/// Get the current store (computed from source inputs).
382-
pub fn store(&self, source_set: SourceFileSet) -> Store {
383-
build_store(self, source_set)
442+
/// Get the current store (computed from source and schema inputs).
443+
pub fn store(&self, source_set: SourceFileSet, schema_set: SchemaInputSet) -> Store {
444+
build_store(self, source_set, schema_set)
384445
}
385446

386447
/// Get the current merged schema (computed from schema inputs).
@@ -618,9 +679,10 @@ artifacts:
618679
fn adding_artifact_appears_in_store() {
619680
let mut db = RivetDatabase::new();
620681
let sources = db.load_sources(&[("reqs.yaml", SOURCE_REQ)]);
682+
let schemas = db.load_schemas(&[("test", TEST_SCHEMA)]);
621683

622684
// Initially: 1 artifact (REQ-001).
623-
let store = db.store(sources);
685+
let store = db.store(sources, schemas);
624686
assert_eq!(store.len(), 1);
625687
assert!(store.contains("REQ-001"));
626688

@@ -636,7 +698,7 @@ artifacts:
636698
"#;
637699
db.update_source(sources, "reqs.yaml", combined.to_string());
638700

639-
let store = db.store(sources);
701+
let store = db.store(sources, schemas);
640702
assert_eq!(store.len(), 2);
641703
assert!(store.contains("REQ-001"));
642704
assert!(store.contains("REQ-002"));

0 commit comments

Comments
 (0)