diff --git a/CHANGELOG.md b/CHANGELOG.md index 4963d47..ed4a17c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,19 @@ ### Fixed +- **REQ-162 / #522 — restore `accepted` to the canonical status enum.** v0.16.0 + declared the canonical lifecycle (`draft → … → released` + `deprecated` / + `rejected`) but dropped `accepted`, the documented terminal state for + `design-decision` / `external-anchor` / requirement-meta artifacts. Downstream + stores that followed the documented chain (e.g. the jess hardware-integration + store, 12 of 21 errors on upgrade) flipped from PASS to FAIL on the 0.15 → 0.16 + bump with no migration path. `accepted` is now re-admitted in + `schemas/common.yaml`, so stores upgrade cleanly; the `status-allowed-values` + guard still fires on genuinely typo'd values (covered by + `common_status_accepts_accepted_and_still_rejects_typos`). The wider + migration / per-schema enum questions raised in #522 (slices 1 and 3) are + tracked separately. + - **REQ-168 / #428 — `rivet bundle --incoming`.** `bundle` built the closure from *outgoing* links only, so bundling a requirement (a graph sink — everything links *to* it, it links *out* to nothing) returned just the bare diff --git a/rivet-core/tests/schema_validation_rules.rs b/rivet-core/tests/schema_validation_rules.rs index 5f1473e..008af5b 100644 --- a/rivet-core/tests/schema_validation_rules.rs +++ b/rivet-core/tests/schema_validation_rules.rs @@ -413,3 +413,75 @@ fn aspice_phase_9_scales_to_100_verifiers() { "full validate of 200-artifact store took {elapsed:?}; suspected O(N²) regression" ); } + +/// #522 slice 2 regression: `status: accepted` is the documented terminal +/// state for design-decision / external-anchor / requirement-meta artifacts. +/// Dropping it from the canonical enum in 0.16.0 flipped existing stores +/// that followed the documented lifecycle (12 of 21 errors on the jess +/// store) from PASS to FAIL. Confirm the enum re-admits it and the +/// `status-allowed-values` rule still fires on a genuinely off-vocabulary +/// value so the typo-guard isn't widened to "anything goes." +// rivet: verifies REQ-162 +#[test] +fn common_status_accepts_accepted_and_still_rejects_typos() { + let schema = Schema::merge(&[parse_schema("common"), parse_schema("dev")]); + + // Acceptance bullet 3: introspection surface must agree with the + // diagnostic. The status field is a schema-wide base-field + // (`schemas/common.yaml` → `base-fields:`); `rivet schema show ` + // surfaces it via `Schema::base_fields`. That list must include + // `accepted`. + let status_field = schema + .base_fields + .iter() + .find(|f| f.name == "status") + .expect("status must be declared as a common.yaml base-field"); + let allowed = status_field + .allowed_values + .as_ref() + .expect("status base-field must declare allowed-values"); + assert!( + allowed.iter().any(|v| v == "accepted"), + "common.yaml status enum must include `accepted` (documented terminal \ + state for design-decision / external-anchor / requirement-meta); got {allowed:?}" + ); + + let mut store = Store::default(); + // Acceptance bullet 2: a design-decision with `status: accepted` must + // validate cleanly — no `allowed-values` / `status-allowed-values` + // diagnostic on it. + store.upsert(artifact("DD-ACCEPTED", "design-decision", "accepted", &[])); + // Negative control: a typo'd status must still fire the rule so the + // canonical-enum guard isn't accidentally widened. + store.upsert(artifact("DD-TYPO", "design-decision", "implmented", &[])); + + let graph = LinkGraph::build(&store, &schema); + let diags = validate(&store, &schema, &graph); + + let dd_accepted_status_diags: Vec<_> = diags + .iter() + .filter(|d| { + d.artifact_id.as_deref() == Some("DD-ACCEPTED") + && (d.rule == "allowed-values" || d.rule == "status-allowed-values") + }) + .collect(); + assert!( + dd_accepted_status_diags.is_empty(), + "design-decision with `status: accepted` must not produce a status \ + allowed-values diagnostic; got {dd_accepted_status_diags:#?}" + ); + + let dd_typo_status_diags: Vec<_> = diags + .iter() + .filter(|d| { + d.artifact_id.as_deref() == Some("DD-TYPO") + && (d.rule == "allowed-values" || d.rule == "status-allowed-values") + }) + .collect(); + assert_eq!( + dd_typo_status_diags.len(), + 1, + "design-decision with a typo'd status must still fire the status \ + allowed-values guard exactly once; got {dd_typo_status_diags:#?}" + ); +} diff --git a/schemas/common.yaml b/schemas/common.yaml index 9fdd96f..1e69540 100644 --- a/schemas/common.yaml +++ b/schemas/common.yaml @@ -50,6 +50,11 @@ base-fields: # (`schemas/aspice.yaml` uses `(= status "released")`) and the # tool-qualification defect flow gates. Absent status is allowed (the check # only fires when a status is present). + # #522: `accepted` is the documented terminal state for design-decision / + # external-anchor / requirement-meta artifacts (the lifecycle docs and + # downstream agent guidance use it) — kept here so stores that followed + # the documented chain `... -> verified -> accepted` don't flip to FAIL + # on the 0.15 -> 0.16 bump. allowed-values: - draft - proposed @@ -57,6 +62,7 @@ base-fields: - implemented - verified - released + - accepted - deprecated - rejected