From 60e383566cf377d8bea26e5edea499e23ef82e85 Mon Sep 17 00:00:00 2001 From: Manuel Retamozo Date: Fri, 28 Aug 2026 00:23:34 +0200 Subject: [PATCH] chore(release): actualizar version a 2.52.0 y changelog Cierra la remediacion semantica K6b (B1-B3/H1-H3: no aliasing, cobertura token a token, digest de contrato, grafo fail-closed) y alinea el roadmap: K6b done, K6c next-eligible. --- .claude-plugin/plugin.json | 2 +- .plugin.json | 2 +- CHANGELOG.md | 11 + ...persist-token-coverage-on-assessment-v1.md | 23 + ...2-ordered-non-aliased-strategy-evidence.md | 23 + ...rity-validation-across-graph-operations.md | 23 + docs/architecture/harness-evolution.md | 6 +- docs/roadmaps/harness-evolution.md | 13 +- .../.4r/candidate.diff | 2314 +++++++++++++++++ .../.4r/decision.json | 166 ++ .../.4r/evidence.json | 69 + .../.4r/gate-plan.json | 194 ++ .../.4r/lineage.json | 271 ++ .../.4r/summary.json | 73 + .../apply-progress.md | 43 + .../archive-plan.json | 78 + .../archive-report.md | 89 + .../decisions/adr-001.md | 23 + .../decisions/adr-002.md | 23 + .../decisions/adr-003.md | 23 + .../design.md | 147 ++ .../prepared-specs/assurance-graph/spec.md | 242 ++ .../independent-verification/spec.md | 281 ++ .../kernel-contract-schemas/spec.md | 856 ++++++ .../proposal.md | 73 + .../specs/assurance-graph/spec.md | 95 + .../specs/independent-verification/spec.md | 115 + .../specs/kernel-contract-schemas/spec.md | 51 + .../state.yaml | 185 ++ .../tasks.md | 91 + .../verify-report.md | 153 ++ openspec/config.yaml | 2 +- openspec/specs/assurance-graph/spec.md | 86 +- .../specs/independent-verification/spec.md | 97 +- .../specs/kernel-contract-schemas/spec.md | 81 +- package.json | 2 +- .../fixtures/invalid/v1-missing-coverage.json | 11 + .../fixtures/invalid/v1-with-verdict.json | 1 + .../fixtures/valid/v1-complete.json | 3 +- .../fixtures/valid/v1-four-roles.json | 12 +- schemas/kernel/assessment/v1.schema.json | 11 +- schemas/kernel/contract-claims.json | 3 +- .../k6b-verifier-assurance-graph-e2e.test.js | 8 +- scripts/lib/assurance-graph/index.js | 83 +- scripts/lib/assurance-graph/index.test.js | 208 +- scripts/lib/assurance-graph/projector.js | 54 +- .../lib/independent-verifier/assessment.js | 44 +- .../independent-verifier/assessment.test.js | 25 +- scripts/lib/independent-verifier/bindings.js | 10 +- scripts/lib/independent-verifier/evidence.js | 4 + .../lib/independent-verifier/index.test.js | 61 +- .../obligation-coverage.js | 20 + .../obligation-coverage.test.js | 26 + .../independent-verifier/strategy-policy.js | 40 + scripts/lib/k2a-maturity-docs.test.js | 4 +- scripts/lib/k6b-schema-fixtures.test.js | 3 + 56 files changed, 6499 insertions(+), 158 deletions(-) create mode 100644 docs/adr/adr-20260828-001-persist-token-coverage-on-assessment-v1.md create mode 100644 docs/adr/adr-20260828-002-ordered-non-aliased-strategy-evidence.md create mode 100644 docs/adr/adr-20260828-003-canonical-integrity-validation-across-graph-operations.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/candidate.diff create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/decision.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/evidence.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/gate-plan.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/lineage.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/summary.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/apply-progress.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/archive-plan.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/archive-report.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/decisions/adr-001.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/decisions/adr-002.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/decisions/adr-003.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/design.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/prepared-specs/assurance-graph/spec.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/prepared-specs/independent-verification/spec.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/prepared-specs/kernel-contract-schemas/spec.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/proposal.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/specs/independent-verification/spec.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/state.yaml create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/tasks.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/verify-report.md create mode 100644 schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 87a5b151..be48b582 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ospec-workflow", "description": "Spec-Driven Development workflow for VS Code Agent Customization with OpenSpec, strict TDD, phase agents, skills, hooks, and verification contracts.", - "version": "2.51.0", + "version": "2.52.0", "author": { "name": "Manuel Michael Retamozo García" }, diff --git a/.plugin.json b/.plugin.json index 87a5b151..be48b582 100644 --- a/.plugin.json +++ b/.plugin.json @@ -1,7 +1,7 @@ { "name": "ospec-workflow", "description": "Spec-Driven Development workflow for VS Code Agent Customization with OpenSpec, strict TDD, phase agents, skills, hooks, and verification contracts.", - "version": "2.51.0", + "version": "2.52.0", "author": { "name": "Manuel Michael Retamozo García" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index e078e438..85451e9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.52.0] - 2026-08-28 + +### Changed +- **Integridad semántica K6b (`k6b-semantic-integrity-remediation`)**: + - Roles de estrategia incompatibles no pueden compartir un `EvidenceId`; el orden temporal RED→GREEN / RED→PATCH→GREEN es fail-closed (`STRATEGY_EVIDENCE_ALIAS`, `STRATEGY_SEQUENCE_VIOLATION`). + - Cobertura MUST token a token persistida en `assessment/v1` (`evidence_requirements_satisfied`); omisión o subconjunto incompleto falla cerrado. + - El digest de contrato se comprueba antes de strategy (`BINDING_MISMATCH` sin verdict). + - Projector, replay y reconcile del Assurance Graph fallan cerrados ante inputs canónicos contradictorios, assessments tampered o payload almacenado incompleto (`GRAPH_DIVERGENCE`). + - `evidence/v2`, `verification/v2` y K1 v1 permanecen byte-identical. ADRs `docs/adr/adr-20260828-001` a `003`. + - K6b queda `done`; K6c pasa a `next-eligible`. Verify: 2762 pass, 0 fail; 4R approved (4 WARNING advisory). Archivado en `openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/`. + ## [2.51.0] - 2026-08-27 ### Added diff --git a/docs/adr/adr-20260828-001-persist-token-coverage-on-assessment-v1.md b/docs/adr/adr-20260828-001-persist-token-coverage-on-assessment-v1.md new file mode 100644 index 00000000..056e9a6e --- /dev/null +++ b/docs/adr/adr-20260828-001-persist-token-coverage-on-assessment-v1.md @@ -0,0 +1,23 @@ +# ADR-001: Persist token coverage on assessment/v1 + +- Status: proposed +- Change: k6b-semantic-integrity-remediation +- Date: 2026-08-27 + +## Context + +Existential evidence binding cannot prove `required_evidence` subset coverage. Coverage belongs to an evaluated evidence/role/obligation tuple, while `evidence/v2` and K1 contracts are frozen. + +## Decision + +Add required `evidence_requirements_satisfied` to `assessment/v1` as a unique, canonical string array. Include it in the `assessment_id` preimage and in assessment required claims. Omission fails schema validation. + +## Alternatives + +- Put tokens on `evidence/v2`: rejected; it mutates observation identity. +- Keep coverage ephemeral: rejected; replay cannot validate it. +- Publish `assessment/v2`: rejected; the accepted delta evolves the additive v1 family and requires no parallel family. + +## Consequences + +Old partial assessments fail closed and must be regenerated. Coverage tampering changes identity. Evidence, verification, and K1 schema bytes stay frozen; rollback is a unit revert. diff --git a/docs/adr/adr-20260828-002-ordered-non-aliased-strategy-evidence.md b/docs/adr/adr-20260828-002-ordered-non-aliased-strategy-evidence.md new file mode 100644 index 00000000..81d534c7 --- /dev/null +++ b/docs/adr/adr-20260828-002-ordered-non-aliased-strategy-evidence.md @@ -0,0 +1,23 @@ +# ADR-002: Ordered, non-aliased strategy evidence + +- Status: proposed +- Change: k6b-semantic-integrity-remediation +- Date: 2026-08-27 + +## Context + +A Set of roles proves neither distinct observations nor temporal order. Adding timestamps to frozen `evidence/v2` would create a new clock contract. + +## Decision + +Evaluate normalized evidence in `rawEvidence` list order. Within the selected strategy, one EvidenceId may bind repeatedly to one role but not to distinct semantic roles. Require RED before GREEN for Strict TDD and RED before PATCH before GREEN for bug strategy. + +## Alternatives + +- Keep role-set evaluation: rejected; aliasing and reversed sequences pass. +- Add persisted timestamps: rejected; mutates the frozen evidence family and introduces clock authority. +- Infer order from digests: rejected; content hashes carry no chronology. + +## Consequences + +Callers must provide evidence in observed sequence and use distinct observations for distinct roles. Reordering becomes semantically observable but remains cheap to correct. Schema-only four-role fixtures remain valid; combined verifier use fails closed. diff --git a/docs/adr/adr-20260828-003-canonical-integrity-validation-across-graph-operations.md b/docs/adr/adr-20260828-003-canonical-integrity-validation-across-graph-operations.md new file mode 100644 index 00000000..7b0935af --- /dev/null +++ b/docs/adr/adr-20260828-003-canonical-integrity-validation-across-graph-operations.md @@ -0,0 +1,23 @@ +# ADR-003: Canonical integrity validation across graph operations + +- Status: proposed +- Change: k6b-semantic-integrity-remediation +- Date: 2026-08-27 + +## Context + +The projector coalesces contradictory canonical inputs, replay trusts assessments, and reconcile compares only graph id plus edges. Direct callers can therefore bypass facade checks or hide stored-payload tampering. + +## Decision + +Share deterministic canonical-input and graph-id helpers. Project rejects contradictory or unresolved digests before hashing; replay schema-validates and recomputes every assessment plus graph binding; reconcile recomputes the stored payload identity and compares the complete canonical graph. + +## Alternatives + +- Harden only `verifyCandidate`: rejected; direct graph APIs remain permissive. +- Compare graph id and edges only: rejected; nodes, subject, canonical inputs, and kind/schema remain unchecked. +- Trust a recomputed assessment id alone: rejected; graph/evidence/obligation bindings still need validation. + +## Consequences + +Project, replay, and reconcile share one fail-closed identity model. Legacy incomplete payloads fail instead of being repaired silently. The change adds validation work but no dependency or migration. diff --git a/docs/architecture/harness-evolution.md b/docs/architecture/harness-evolution.md index 71cb5aad..0d29d7bb 100644 --- a/docs/architecture/harness-evolution.md +++ b/docs/architecture/harness-evolution.md @@ -1,8 +1,8 @@ # Arquitectura objetivo — harness gobernado por kernel, grafo y evidencia > **Autoridad:** fuente conceptual y estratégica del harness (responsabilidades y límites). -> **Corte documental:** v2.51.0, 2026-08-27 (estado alineado al roadmap; la dirección conceptual no cambia). -> **Estado verificado:** O3, O4+O5/O4.1, O4.2, O6A, O2B, **K1**, **K2**, **K2.1**, **K2a**, **K3**, **`k3-readiness-remediation`**, **K4a**, **K5**, **K6a**, **K4b** y **K6b** (archivado v2.50.0; integridad cerrada en v2.51.0). OpenSpec/Git/Candidate siguen siendo la única autoridad semántica; el Assurance Graph es proyección. **K6c** es `next-eligible`. +> **Corte documental:** v2.52.0, 2026-08-28 (estado alineado al roadmap; la dirección conceptual no cambia). +> **Estado verificado:** O3, O4+O5/O4.1, O4.2, O6A, O2B, **K1**, **K2**, **K2.1**, **K2a**, **K3**, **`k3-readiness-remediation`**, **K4a**, **K5**, **K6a**, **K4b** y **K6b** están cerrados. OpenSpec/Git/Candidate siguen siendo la única autoridad semántica; el Assurance Graph es proyección. **K6c** queda `next-eligible`. > **Roadmap:** orden, estado operativo y done criteria viven en [`../roadmaps/harness-evolution.md`](../roadmaps/harness-evolution.md). > **Precedencia documental:** ante diferencias de **orden o estado**, prevalece el roadmap; ante diferencias **conceptuales**, reconciliar antes de iniciar el slice. > **Investigación no normativa:** la trazabilidad completa P0–P27 vive en [`research/harness-kernel-graph-evidence-roadmap-fusion.md`](research/harness-kernel-graph-evidence-roadmap-fusion.md). La proporcionalidad de proceso y el programa de changes viven en [`research/proportional-process-and-change-program.md`](research/proportional-process-and-change-program.md). @@ -855,7 +855,7 @@ Repositorios fixture reciben 10–30 cambios consecutivos. Se miden duplicación 8. ~~K5: budgets (incl. autoridad/efectos) / failure / recovery~~ — hecho: archivado y publicado en v2.45.13 (remediaciones v2.45.7→v2.45.13). 9. ~~K6a: primitivas de ejecución aislada (`CreateWorkspace`…`DisposeWorkspace`); no conoce Repair~~ — hecho: archivado y publicado en v2.46.7; frontera de procesos cerrada en v2.47.1; endurecimiento de frontera (política inmutable, fs mutante, live-identity, `worker_threads`) en v2.47.2. 10. ~~K4b: orquesta Repair shadow (consume K6a; freeze Candidate vía K3)~~ — hecho: publicado en v2.48.0; corrección en v2.48.1; invariantes de integración en v2.48.2; cierre mode-only/baseline en v2.48.3. -11. ~~K6b: verifier + provenance + Assurance Graph (proyección)~~ — hecho: publicado en v2.50.0; integridad (MUST, assessment/v1, collector, graph_id canónico, proyección fail-closed) cerrada en v2.51.0. K6c ChallengePlan es `next-eligible`; K6d complexity delta sigue pendiente. +11. ~~K6b: verifier + provenance + Assurance Graph (proyección)~~ — publicado en v2.50.0; integridad semántica B1–B3/H1–H3 cerrada en v2.52.0. K6c ChallengePlan queda `next-eligible`; K6d complexity delta sigue pendiente. 12. K7: ReviewAdapter + ReviewReducer + lineage; K8: CandidateEvaluationAttestation (emisión CAS). 13. K9: shadow/replay/A-B; promoción de **un** profile (checkpoints intermedios ya validados). 14. K10-delivery: DeliveryAuthorization **solo** del profile promovido; relación Candidate por etapas; resto fixed/deferred. diff --git a/docs/roadmaps/harness-evolution.md b/docs/roadmaps/harness-evolution.md index 57f5adf5..7305bfba 100644 --- a/docs/roadmaps/harness-evolution.md +++ b/docs/roadmaps/harness-evolution.md @@ -1,7 +1,7 @@ # Roadmap general — kernel, grafo y evidencia > **Autoridad:** única fuente operativa del backlog transversal. -> **Versión de referencia:** v2.51.0, 2026-08-27. +> **Versión de referencia:** v2.52.0, 2026-08-28. > **Arquitectura:** [`../architecture/harness-evolution.md`](../architecture/harness-evolution.md). > **Investigación no normativa:** [`../architecture/research/harness-kernel-graph-evidence-roadmap-fusion.md`](../architecture/research/harness-kernel-graph-evidence-roadmap-fusion.md) (P0–P27). Proporcionalidad de proceso y Change Program: [`../architecture/research/proportional-process-and-change-program.md`](../architecture/research/proportional-process-and-change-program.md). > **Regla de estado:** los hechos se contrastan con código/OpenSpec; este roadmap no cambia el estado de un change ni sustituye sus artefactos. @@ -78,8 +78,8 @@ Las iniciativas anteriores no se descartan. O20A, O13A–C, O15, O18, O19A/B y R | `done` | **K5** | Budgets (incl. autoridad/efectos), failures y recovery; remediaciones v2.45.7→v2.45.13 (authoritative enforcement, authority boundary/CAS concurrency, reconciliación, remediación técnica del núcleo y blindaje de concurrencia); archivado y publicado en v2.45.13 | | `done` | **K6a** | Worker isolation y work-order capsule; primitivas de ejecución aislada, integración con WorkerTransport, contención de filesystem y WorkResult canónico; archivado en v2.46.0, frontera de procesos cerrada en v2.47.1 y endurecida en v2.47.2 | | `done` | **K4b** | Repair shadow execution (WO→WR→integrate→Candidate); despacho exclusivo K6a, integración estricta, cápsula mínima, base derivada y registro 1:N; remediación de invariantes en v2.48.2 y cierre mode-only/baseline en v2.48.3 (`2026-08-26-k4b-mode-only-and-baseline-projection`) | -| `done` | **K6b** | Verifier independiente, evidence strategies/provenance y Assurance Graph; integridad cerrada en v2.51.0 (`k6b-verification-integrity-remediation`) | -| `next-eligible` | **K6c** | ChallengePlan policy-selected | +| `done` | **K6b** | Verifier independiente, strategies/provenance y Assurance Graph proyección; integridad semántica B1–B3/H1–H3 cerrada en v2.52.0 (`2026-08-27-k6b-semantic-integrity-remediation`) | +| `next-eligible` | **K6c** | ChallengePlan policy-selected; desbloqueado tras archive de K6b | | `pending` | K6d–K8 | Complexity delta, review authority, **Evaluation Attestation** | | `pending` | K9 | Gate de promoción shadow/replay/A-B (checkpoints intermedios ya validados) | | `pending` | K10-delivery | `DeliveryAuthorization` **acotada al profile K9**; relación Candidate por etapas; fixed/deferred para el resto | @@ -1011,7 +1011,7 @@ Vertical Repair shadow produce Candidate congelado. Gate de invariantes cerrado **Dependencias:** K4b + K6a + K3. -**Estado:** `done`. Publicado en v2.50.0 (`k6b-verifier-evidence-assurance-graph`); integridad cerrada en v2.51.0 (`k6b-verification-integrity-remediation`): cobertura MUST del Obligation Manifest, assessments persistibles `assessment/v1`, provenance de collector del harness, `graph_id` canónico y proyección fail-closed. Este change no reabre el macro-slice. OpenSpec/Git/Candidate siguen siendo la única autoridad semántica. El grafo no concede lifecycle, approval ni delivery. **K6c es `next-eligible`.** +**Estado:** `done`. Publicado en v2.50.0 (`k6b-verifier-evidence-assurance-graph`); la remediación semántica focal B1–B3/H1–H3 (`k6b-semantic-integrity-remediation`) cierra cobertura MUST, assessments persistibles `assessment/v1`, `graph_id` canónico y proyección/replay/reconciliación fail-closed en v2.52.0. Este change no reabre el macro-slice. OpenSpec/Git/Candidate siguen siendo la única autoridad semántica. El grafo no concede lifecycle, approval ni delivery. **K6c queda `next-eligible`.** **Absorbe/rebasa:** P12/P16; O15; separación apply/verify vigente. @@ -1103,7 +1103,7 @@ external-unverified - equivalence manifest queda listo para K9; - ningún consumer trata el Assurance Graph como segunda fuente de verdad frente a OpenSpec/Git/Candidate. -**Gate terminal:** verifier, strategy selector, provenance y Assurance Graph (proyección) conformes; checkpoint evidencia/challenges → `continue` | `revise` | `reject`. Cerrado en v2.51.0. Desbloquea K6c. +**Gate terminal:** verifier, strategy selector, provenance y Assurance Graph (proyección) conformes; checkpoint evidencia/challenges → `continue` | `revise` | `reject`. Cerrado en v2.52.0. Desbloquea K6c. ### K6c — adversarial challenges (policy-selected) — **next-eligible** @@ -2061,4 +2061,5 @@ Un Change Program (objetivo → children OpenSpec + cursor, ver investigación ` - 2026-08-27: K6b (`k6b-verifier-evidence-assurance-graph`) entra en apply: verifier independiente, evidence strategies/provenance y Assurance Graph proyección `implemented`; autoridad independiente del grafo, K6c/K7/K8 permanecen `target`. K6c queda next-eligible. - 2026-08-27: K6b cierra con verify PASS, 4R approved y archive transaccional; publicado en v2.50.0. K6c queda next-eligible. - 2026-08-27: K6b entra en `revise` (`k6b-verification-integrity-remediation`): cobertura MUST, assessments persistibles, provenance de collector, `graph_id` canónico y proyección fail-closed. K6c pasa a `blocked-by-K6b-remediation` hasta archive de esa remediación. -- 2026-08-27: K6b (`k6b-verification-integrity-remediation`) cierra integridad con verify PASS, 4R successor approved y archive transaccional; publicado en v2.51.0. K6b queda `done`; K6c queda next-eligible. +- 2026-08-27: K6b (`k6b-semantic-integrity-remediation`) entra en apply para remediar B1–B3/H1–H3; K6b queda `revise` y K6c `blocked-by-K6b-remediation` hasta archive. +- 2026-08-28: K6b (`k6b-semantic-integrity-remediation`) cierra con verify PASS, 4R approved y archive transaccional; publicado en v2.52.0. K6c queda next-eligible. diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/candidate.diff b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/candidate.diff new file mode 100644 index 00000000..ffc06128 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/candidate.diff @@ -0,0 +1,2314 @@ +diff --git a/docs/architecture/harness-evolution.md b/docs/architecture/harness-evolution.md +index 71cb5aa..3657217 100644 +--- a/docs/architecture/harness-evolution.md ++++ b/docs/architecture/harness-evolution.md +@@ -2,7 +2,7 @@ + + > **Autoridad:** fuente conceptual y estratégica del harness (responsabilidades y límites). + > **Corte documental:** v2.51.0, 2026-08-27 (estado alineado al roadmap; la dirección conceptual no cambia). +-> **Estado verificado:** O3, O4+O5/O4.1, O4.2, O6A, O2B, **K1**, **K2**, **K2.1**, **K2a**, **K3**, **`k3-readiness-remediation`**, **K4a**, **K5**, **K6a**, **K4b** y **K6b** (archivado v2.50.0; integridad cerrada en v2.51.0). OpenSpec/Git/Candidate siguen siendo la única autoridad semántica; el Assurance Graph es proyección. **K6c** es `next-eligible`. ++> **Estado verificado:** O3, O4+O5/O4.1, O4.2, O6A, O2B, **K1**, **K2**, **K2.1**, **K2a**, **K3**, **`k3-readiness-remediation`**, **K4a**, **K5**, **K6a** y **K4b** están cerrados; **K6b** está en `revise` por remediación semántica focal. OpenSpec/Git/Candidate siguen siendo la única autoridad semántica; el Assurance Graph es proyección. **K6c** queda `blocked-by-K6b-remediation` hasta archive. + > **Roadmap:** orden, estado operativo y done criteria viven en [`../roadmaps/harness-evolution.md`](../roadmaps/harness-evolution.md). + > **Precedencia documental:** ante diferencias de **orden o estado**, prevalece el roadmap; ante diferencias **conceptuales**, reconciliar antes de iniciar el slice. + > **Investigación no normativa:** la trazabilidad completa P0–P27 vive en [`research/harness-kernel-graph-evidence-roadmap-fusion.md`](research/harness-kernel-graph-evidence-roadmap-fusion.md). La proporcionalidad de proceso y el programa de changes viven en [`research/proportional-process-and-change-program.md`](research/proportional-process-and-change-program.md). +@@ -21,7 +21,7 @@ Sin duplicar el backlog: solo responsabilidades y límites alineados al roadmap + + | Tema | Decisión arquitectónica | + | --- | --- | +-| Estado | K1+K2+K2.1+K2a+K3+`k3-readiness-remediation`+K4a+K5+K6a+K4b+K6b `done`; **K6c** `next-eligible` | ++| Estado | K1+K2+K2.1+K2a+K3+`k3-readiness-remediation`+K4a+K5+K6a+K4b `done`; **K6b** `revise`; **K6c** `blocked-by-K6b-remediation` hasta archive | + | Dos grafos | **Execution Graph** (trabajo) ≠ **Assurance Graph** (fiabilidad / evidencia; no “prueba formal”) | + | Identidades | `SourceSnapshotId` / `WorkOrderId` / `WorkResultId` / `CandidateId` (sin IDs nuevos por ahora) | + | Relación Candidate | Inicial: `exact` / `changed` / `ambiguous` / `unknown`; `compatible-base-advance` experimental hasta K9 | +@@ -855,7 +855,7 @@ Repositorios fixture reciben 10–30 cambios consecutivos. Se miden duplicación + 8. ~~K5: budgets (incl. autoridad/efectos) / failure / recovery~~ — hecho: archivado y publicado en v2.45.13 (remediaciones v2.45.7→v2.45.13). + 9. ~~K6a: primitivas de ejecución aislada (`CreateWorkspace`…`DisposeWorkspace`); no conoce Repair~~ — hecho: archivado y publicado en v2.46.7; frontera de procesos cerrada en v2.47.1; endurecimiento de frontera (política inmutable, fs mutante, live-identity, `worker_threads`) en v2.47.2. + 10. ~~K4b: orquesta Repair shadow (consume K6a; freeze Candidate vía K3)~~ — hecho: publicado en v2.48.0; corrección en v2.48.1; invariantes de integración en v2.48.2; cierre mode-only/baseline en v2.48.3. +-11. ~~K6b: verifier + provenance + Assurance Graph (proyección)~~ — hecho: publicado en v2.50.0; integridad (MUST, assessment/v1, collector, graph_id canónico, proyección fail-closed) cerrada en v2.51.0. K6c ChallengePlan es `next-eligible`; K6d complexity delta sigue pendiente. ++11. ~~K6b: verifier + provenance + Assurance Graph (proyección)~~ — publicado en v2.50.0; la remediación semántica focal B1–B3/H1–H3 permanece en `revise` hasta archive. K6c ChallengePlan queda `blocked-by-K6b-remediation`; K6d complexity delta sigue pendiente. + 12. K7: ReviewAdapter + ReviewReducer + lineage; K8: CandidateEvaluationAttestation (emisión CAS). + 13. K9: shadow/replay/A-B; promoción de **un** profile (checkpoints intermedios ya validados). + 14. K10-delivery: DeliveryAuthorization **solo** del profile promovido; relación Candidate por etapas; resto fixed/deferred. +diff --git a/docs/roadmaps/harness-evolution.md b/docs/roadmaps/harness-evolution.md +index 57f5adf..3c19dad 100644 +--- a/docs/roadmaps/harness-evolution.md ++++ b/docs/roadmaps/harness-evolution.md +@@ -78,8 +78,8 @@ Las iniciativas anteriores no se descartan. O20A, O13A–C, O15, O18, O19A/B y R + | `done` | **K5** | Budgets (incl. autoridad/efectos), failures y recovery; remediaciones v2.45.7→v2.45.13 (authoritative enforcement, authority boundary/CAS concurrency, reconciliación, remediación técnica del núcleo y blindaje de concurrencia); archivado y publicado en v2.45.13 | + | `done` | **K6a** | Worker isolation y work-order capsule; primitivas de ejecución aislada, integración con WorkerTransport, contención de filesystem y WorkResult canónico; archivado en v2.46.0, frontera de procesos cerrada en v2.47.1 y endurecida en v2.47.2 | + | `done` | **K4b** | Repair shadow execution (WO→WR→integrate→Candidate); despacho exclusivo K6a, integración estricta, cápsula mínima, base derivada y registro 1:N; remediación de invariantes en v2.48.2 y cierre mode-only/baseline en v2.48.3 (`2026-08-26-k4b-mode-only-and-baseline-projection`) | +-| `done` | **K6b** | Verifier independiente, evidence strategies/provenance y Assurance Graph; integridad cerrada en v2.51.0 (`k6b-verification-integrity-remediation`) | +-| `next-eligible` | **K6c** | ChallengePlan policy-selected | ++| `revise` | **K6b** | Remediación semántica focal B1–B3/H1–H3 en curso (`k6b-semantic-integrity-remediation`) | ++| `blocked-by-K6b-remediation` | **K6c** | ChallengePlan policy-selected; bloqueado hasta archive de K6b | + | `pending` | K6d–K8 | Complexity delta, review authority, **Evaluation Attestation** | + | `pending` | K9 | Gate de promoción shadow/replay/A-B (checkpoints intermedios ya validados) | + | `pending` | K10-delivery | `DeliveryAuthorization` **acotada al profile K9**; relación Candidate por etapas; fixed/deferred para el resto | +@@ -186,7 +186,7 @@ Campo canónico de binding al candidato: **`candidate_id`** (no `candidate_diges + Entregado: + G0/G0.1 ─ O2A ─ O3 ─ O4+O5/O4.1 ─ O4.2 ─ O6A ─ O2B → K1 → K2 → K2.1 → K2a → K3 → K4a → K5 → K6a → K4b → K6b + ↓ +-Next: K6c (next-eligible) ++Next: K6c (blocked-by-K6b-remediation) + ↓ + Pending: K6d → K7 → K8 + ↓ +@@ -1007,11 +1007,11 @@ Take compiled Repair Execution Graph (K4a) + + Vertical Repair shadow produce Candidate congelado. Gate de invariantes cerrado en v2.48.3: mode-only exige path existente y `old mode` de la base; la comparación baseline es graph-bound sin préstamo del Graph shadow. Desbloquea K6b. El resultado de O20A decide **promover, revisar o rechazar** el kernel común solo tras K9; rechazo conserva fixed. + +-### K6b — verifier independiente, evidence strategies y Assurance Graph — **done** ++### K6b — verifier independiente, evidence strategies y Assurance Graph — **revise** + + **Dependencias:** K4b + K6a + K3. + +-**Estado:** `done`. Publicado en v2.50.0 (`k6b-verifier-evidence-assurance-graph`); integridad cerrada en v2.51.0 (`k6b-verification-integrity-remediation`): cobertura MUST del Obligation Manifest, assessments persistibles `assessment/v1`, provenance de collector del harness, `graph_id` canónico y proyección fail-closed. Este change no reabre el macro-slice. OpenSpec/Git/Candidate siguen siendo la única autoridad semántica. El grafo no concede lifecycle, approval ni delivery. **K6c es `next-eligible`.** ++**Estado:** `revise`. Publicado en v2.50.0 (`k6b-verifier-evidence-assurance-graph`); la remediación semántica focal B1–B3/H1–H3 (`k6b-semantic-integrity-remediation`) endurece cobertura MUST, assessments persistibles `assessment/v1`, `graph_id` canónico y proyección/replay/reconciliación fail-closed. Este change no reabre el macro-slice. OpenSpec/Git/Candidate siguen siendo la única autoridad semántica. El grafo no concede lifecycle, approval ni delivery. **K6c queda `blocked-by-K6b-remediation` hasta archive.** + + **Absorbe/rebasa:** P12/P16; O15; separación apply/verify vigente. + +@@ -1105,7 +1105,7 @@ external-unverified + + **Gate terminal:** verifier, strategy selector, provenance y Assurance Graph (proyección) conformes; checkpoint evidencia/challenges → `continue` | `revise` | `reject`. Cerrado en v2.51.0. Desbloquea K6c. + +-### K6c — adversarial challenges (policy-selected) — **next-eligible** ++### K6c — adversarial challenges (policy-selected) — **blocked-by-K6b-remediation** + + **Dependencias:** K6b (+ `PolicySnapshot` / strategy de evidencia). + +@@ -2061,4 +2061,4 @@ Un Change Program (objetivo → children OpenSpec + cursor, ver investigación ` + - 2026-08-27: K6b (`k6b-verifier-evidence-assurance-graph`) entra en apply: verifier independiente, evidence strategies/provenance y Assurance Graph proyección `implemented`; autoridad independiente del grafo, K6c/K7/K8 permanecen `target`. K6c queda next-eligible. + - 2026-08-27: K6b cierra con verify PASS, 4R approved y archive transaccional; publicado en v2.50.0. K6c queda next-eligible. + - 2026-08-27: K6b entra en `revise` (`k6b-verification-integrity-remediation`): cobertura MUST, assessments persistibles, provenance de collector, `graph_id` canónico y proyección fail-closed. K6c pasa a `blocked-by-K6b-remediation` hasta archive de esa remediación. +-- 2026-08-27: K6b (`k6b-verification-integrity-remediation`) cierra integridad con verify PASS, 4R successor approved y archive transaccional; publicado en v2.51.0. K6b queda `done`; K6c queda next-eligible. ++- 2026-08-27: K6b (`k6b-semantic-integrity-remediation`) entra en apply para remediar B1–B3/H1–H3; K6b queda `revise` y K6c `blocked-by-K6b-remediation` hasta archive. +diff --git a/openspec/changes/k6b-semantic-integrity-remediation/apply-progress.md b/openspec/changes/k6b-semantic-integrity-remediation/apply-progress.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-semantic-integrity-remediation/apply-progress.md +@@ -0,0 +1,43 @@ ++# Apply Progress: k6b Semantic Integrity Remediation ++ ++## Batch 1 — 2026-08-27 ++ ++Delivery path: `size:exception` accepted by the maintainer. Scope remained focal to B1–B3/H1–H3; 22/22 checklist tasks are implemented and locally verified. (The task artifact contains 22 items despite the prior forecast referring to 21.) ++ ++| Task range | Status | Local verification | Result | ++| --- | --- | --- | --- | ++| 1.1–1.5 | [x] | `node --test ...k6b-schema-fixtures.test.js ...assessment.test.js` | Required canonical coverage is schema-valid, included in assessment identity, and frozen K1/v2 bytes remain pinned. | ++| 2.1–2.7 | [x] | `node --test scripts/lib/independent-verifier/*.test.js` | Contract gate runs before strategy; incompatible evidence roles, invalid order, unknown/wrong bindings, and partial token coverage fail closed. | ++| 3.1–3.6 | [x] | `node --test scripts/lib/assurance-graph/index.test.js` | Project, replay, and reconcile validate canonical digests and complete stored graph payloads fail-closed. | ++| 4.1–4.4 | [x] | Focused suite: 79/79 tests passed; full `npm test` completed with `All checks passed.` | E2E is deterministic; K6b is `revise` and K6c blocked until archive; no legacy assessment migration was introduced. | ++ ++## Files changed ++ ++- Assessment contract and fixtures: `schemas/kernel/assessment/v1.schema.json`, `schemas/kernel/contract-claims.json`, and assessment fixtures. ++- Verifier: canonical assessment validation, contract binding, evidence coverage normalization, ordered non-aliased strategy checks, and token-subset MUST coverage. ++- Assurance Graph: resolved canonical-input validation, replay assessment revalidation, stored-payload graph-id recomputation, and complete reconciliation comparison. ++- Tests and E2E coverage under `scripts/lib/**` and `scripts/k6b-verifier-assurance-graph-e2e.test.js`. ++- Roadmap and architecture status for the active remediation. ++ ++## Migration and rollback ++ ++No migration is attempted. Legacy `assessment/v1` records without `evidence_requirements_satisfied` fail closed and must be regenerated from canonical verifier inputs. Roll back schema, verifier, graph hardening, fixtures, tests, and temporary roadmap state together; `evidence/v2`, `verification/v2`, and K1 v1 remain unmodified. ++ ++## Deviations and risks ++ ++None from the approved design. The repository had a pre-existing `models.yaml` modification; it was preserved and is outside this change scope. The implementation delta is approximately 450 changed lines, below the 850–1,150 forecast, so no workload escalation occurred. ++ ++## Batch 2 — 2026-08-27 ++ ++Delivery path remains the maintainer-approved `size:exception`. This focused remediation completes 4/4 verify-gap tasks (26/26 total) with persistent runtime tests only; no production source, schema, fixture, or contract file changed. ++ ++| Task range | Status | Local verification | Result | ++| --- | --- | --- | --- | ++| 2.8 | [x] | `node --test scripts/lib/independent-verifier/assessment.test.js scripts/lib/assurance-graph/index.test.js` (21/21) | Assessment identity changes independently for `evidence_id` and `obligation_id`. | ++| 3.7 | [x] | Focused verifier/Assurance Graph suite (72/72) | Replay rejects malformed schema, invalid coverage, candidate/policy mismatch, missing evidence, unknown obligation, non-implementing node, and `node_id` mismatch with `GRAPH_DIVERGENCE`. | ++| 3.8 | [x] | Focused verifier/Assurance Graph suite (72/72) | Reconcile rejects tampered stored canonical inputs, candidate, kind, and schema after recomputing the stored graph id. | ++| 5.1 | [x] | `npm test` (exit 0); `git diff --check` | Full suite passed; no scoped implementation mismatch was exposed. | ++ ++## Batch 2 deviations and risks ++ ++None. The new tests exercised the existing fail-closed implementation without requiring a production-code correction. Pre-existing changes, including `models.yaml`, remain preserved and outside this batch. +diff --git a/openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-001.md b/openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-001.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-001.md +@@ -0,0 +1,23 @@ ++# ADR-001: Persist token coverage on assessment/v1 ++ ++- Status: proposed ++- Change: k6b-semantic-integrity-remediation ++- Date: 2026-08-27 ++ ++## Context ++ ++Existential evidence binding cannot prove `required_evidence` subset coverage. Coverage belongs to an evaluated evidence/role/obligation tuple, while `evidence/v2` and K1 contracts are frozen. ++ ++## Decision ++ ++Add required `evidence_requirements_satisfied` to `assessment/v1` as a unique, canonical string array. Include it in the `assessment_id` preimage and in assessment required claims. Omission fails schema validation. ++ ++## Alternatives ++ ++- Put tokens on `evidence/v2`: rejected; it mutates observation identity. ++- Keep coverage ephemeral: rejected; replay cannot validate it. ++- Publish `assessment/v2`: rejected; the accepted delta evolves the additive v1 family and requires no parallel family. ++ ++## Consequences ++ ++Old partial assessments fail closed and must be regenerated. Coverage tampering changes identity. Evidence, verification, and K1 schema bytes stay frozen; rollback is a unit revert. +diff --git a/openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-002.md b/openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-002.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-002.md +@@ -0,0 +1,23 @@ ++# ADR-002: Ordered, non-aliased strategy evidence ++ ++- Status: proposed ++- Change: k6b-semantic-integrity-remediation ++- Date: 2026-08-27 ++ ++## Context ++ ++A Set of roles proves neither distinct observations nor temporal order. Adding timestamps to frozen `evidence/v2` would create a new clock contract. ++ ++## Decision ++ ++Evaluate normalized evidence in `rawEvidence` list order. Within the selected strategy, one EvidenceId may bind repeatedly to one role but not to distinct semantic roles. Require RED before GREEN for Strict TDD and RED before PATCH before GREEN for bug strategy. ++ ++## Alternatives ++ ++- Keep role-set evaluation: rejected; aliasing and reversed sequences pass. ++- Add persisted timestamps: rejected; mutates the frozen evidence family and introduces clock authority. ++- Infer order from digests: rejected; content hashes carry no chronology. ++ ++## Consequences ++ ++Callers must provide evidence in observed sequence and use distinct observations for distinct roles. Reordering becomes semantically observable but remains cheap to correct. Schema-only four-role fixtures remain valid; combined verifier use fails closed. +diff --git a/openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-003.md b/openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-003.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-003.md +@@ -0,0 +1,23 @@ ++# ADR-003: Canonical integrity validation across graph operations ++ ++- Status: proposed ++- Change: k6b-semantic-integrity-remediation ++- Date: 2026-08-27 ++ ++## Context ++ ++The projector coalesces contradictory canonical inputs, replay trusts assessments, and reconcile compares only graph id plus edges. Direct callers can therefore bypass facade checks or hide stored-payload tampering. ++ ++## Decision ++ ++Share deterministic canonical-input and graph-id helpers. Project rejects contradictory or unresolved digests before hashing; replay schema-validates and recomputes every assessment plus graph binding; reconcile recomputes the stored payload identity and compares the complete canonical graph. ++ ++## Alternatives ++ ++- Harden only `verifyCandidate`: rejected; direct graph APIs remain permissive. ++- Compare graph id and edges only: rejected; nodes, subject, canonical inputs, and kind/schema remain unchecked. ++- Trust a recomputed assessment id alone: rejected; graph/evidence/obligation bindings still need validation. ++ ++## Consequences ++ ++Project, replay, and reconcile share one fail-closed identity model. Legacy incomplete payloads fail instead of being repaired silently. The change adds validation work but no dependency or migration. +diff --git a/openspec/changes/k6b-semantic-integrity-remediation/design.md b/openspec/changes/k6b-semantic-integrity-remediation/design.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-semantic-integrity-remediation/design.md +@@ -0,0 +1,147 @@ ++# Design: k6b-semantic-integrity-remediation ++ ++## Technical Approach ++ ++Harden the existing CommonJS verifier and Assurance Graph pipeline in place. The change adds three ordered gates before a successful verdict: canonical contract binding, strategy semantics (non-aliased roles and temporal order), and token-level MUST coverage. Persisted `assessment/v1` records gain the required `evidence_requirements_satisfied` field; the sorted unique value participates in `assessment_id`. `evidence/v2`, `verification/v2`, and all K1 v1 bytes/pins remain unchanged. ++ ++The Assurance Graph projector will validate resolved canonical inputs before hashing. Replay will schema-validate and recompute every assessment before projection. Reconcile will validate the entire stored payload and independently recompute its declared `graph_id`, rather than trusting `graph_id` plus edges. ++ ++This confirms `sdd-propose-001` and `sdd-spec-001`: coverage stays on `assessment/v1`, the exact field is `evidence_requirements_satisfied`, and it is schema-required. ++ ++## Architecture Decisions ++ ++### Decision: Required canonical coverage on `assessment/v1` (ADR-001) ++ ++**Choice**: Add required `evidence_requirements_satisfied` to `assessment/v1`; canonicalize it and include it in `assessment_id`. ++ ++**Alternatives considered**: Mutating frozen `evidence/v2`; keeping coverage ephemeral; publishing a parallel assessment version. ++ ++**Rationale**: Coverage belongs to an evidence/role/obligation binding. Persisting it makes partial coverage and tampering replay-verifiable, at the cost of regenerating old partial assessments. ++ ++### Decision: Strategy role identity and order are evaluated before MUST coverage (ADR-002) ++ ++**Choice**: Use normalized EvidenceIds and `rawEvidence` order. Distinct roles cannot share one id; `bug` requires RED* < PATCH* < GREEN* and `strict-tdd` RED* < GREEN*. ++ ++**Alternatives considered**: A role Set, which loses identity/order; timestamps, which mutate frozen evidence and create clock authority; digest ordering, which has no chronology. ++ ++**Rationale**: The existing input is ordered and repeated evidence for one role can remain valid. Strategy violations fail before the MUST walk. ++ ++### Decision: One canonical integrity path for project, replay, and reconcile (ADR-003) ++ ++**Choice**: Share canonical-input and graph-id helpers across project, replay, and reconcile. ++ ++**Alternatives considered**: Hardening only `verifyCandidate`; retaining graph-id-plus-edges comparison; trusting recomputed assessment ids without graph binding checks. ++ ++**Rationale**: Direct graph APIs are public. Project rejects bad digests, replay validates assessments, and reconcile recomputes and compares the complete stored payload. ++ ++## Data Flow ++ ++```text ++verifyCandidate ++ -> validateBindings ++ -> input.contract.contract_digest == executionGraph.contract_digest ++ -> normalize rawEvidence in caller-supplied list order ++ -> evidence/v2 (unchanged) + role/obligation/coverage binding metadata ++ -> evaluateStrategy ++ -> one EvidenceId cannot cover distinct strategy roles ++ -> bug: RED* < PATCH* < GREEN*; strict-tdd: RED* < GREEN* ++ -> walkMustObligations ++ -> union evidence_requirements_satisfied per obligation ++ -> required_evidence subset check ++ -> emit assessment/v1 for each contributing tuple ++ -> emit verification/v2 ++ -> projectAssuranceGraph ++ -> validate canonical digests -> canonical nodes/edges -> graph_id ++``` ++ ++```text ++persisted candidate + graph + evidence + assessments + verification ++ -> replayAssuranceGraph ++ -> schema + assessment_id + candidate/policy/evidence/node/obligation checks ++ -> token-level MUST coverage check ++ -> projectAssuranceGraph ++ -> reconcileAssuranceGraph(stored, canonical input) ++ -> recompute stored graph_id from stored payload ++ -> compare full stored payload with current canonical projection ++ -> any mismatch: GRAPH_DIVERGENCE ++``` ++ ++All four resolved canonical values must be SHA-256 strings; supplied values must equal authoritative Graph/contract bindings. For coverage, the MUST walk intersects each binding's tokens with the obligation requirements, persists relevant sorted tokens, and checks their union. Missing tokens return `UNFULFILLED_MUST`; existing codes remain for unknown obligations, wrong nodes, and provenance failure. ++ ++## File Changes ++ ++| File | Action | Description | ++| --- | --- | --- | ++| `scripts/lib/independent-verifier/{bindings,evidence,strategy-policy}.js` | Modify | Contract gate; normalized coverage; role identity and sequence. | ++| `scripts/lib/independent-verifier/{assessment,obligation-coverage,index}.js` | Modify | Canonical assessment validation, token-subset walk, ordered facade gates. | ++| `scripts/lib/assurance-graph/{projector,index}.js` | Modify | Strict canonical inputs, assessment replay, full stored-graph reconciliation. | ++| `schemas/kernel/assessment/v1.schema.json` | Modify | Require closed-array field `evidence_requirements_satisfied`. | ++| `schemas/kernel/contract-claims.json` | Modify | Add the field to assessment required claims; leave manifest `$id` unchanged. | ++| `schemas/kernel/assessment/fixtures/{valid,invalid}/*.json` | Modify/Create | Complete/four-role coverage; verdict, cross-family, and missing-coverage failures. | ++| `scripts/lib/independent-verifier/*.test.js` | Modify | Identity, subset, aliasing, digest, and sequence adversaries. | ++| `scripts/lib/assurance-graph/index.test.js` | Modify | Input, assessment, replay, and stored-payload tampering. | ++| `scripts/lib/k6b-schema-fixtures.test.js` | Modify | Claims/fixtures and frozen-contract pins. | ++| `scripts/k6b-verifier-assurance-graph-e2e.test.js` | Modify | Complete verify/project/replay/reconcile path. | ++| `docs/architecture/harness-evolution.md` | Modify | Mark K6b `revise` and K6c blocked until remediation archive. | ++| `docs/roadmaps/harness-evolution.md` | Modify | Apply the same temporary dependency status at roadmap checkpoints. | ++ ++## Interfaces / Contracts ++ ++```javascript ++// Raw verifier binding metadata; evidence/v2 remains unchanged. ++{ ++ role: "acceptance", ++ obligation_ids: ["req-repair-001"], ++ evidence_requirements_satisfied: ["ev:test-pass"], ++ bytes, origin, node_id ++} ++ ++// ospec://schemas/kernel/assessment/v1 ++{ ++ schema_version: 1, ++ kind: "assessment/v1", ++ assessment_id, ++ evidence_id, ++ role, ++ obligation_id, ++ node_id, ++ candidate_id, ++ policy_snapshot_id, ++ evidence_requirements_satisfied: ["ev:test-pass"] ++} ++``` ++ ++`computeAssessmentId` fingerprints every field above except `assessment_id`, with coverage sorted and deduplicated first. The assessment schema remains `additionalProperties: false` and forbids `verdict`. Direct schema validation of four same-evidence role records remains valid; `verifyCandidate` rejects their combined semantic use because schema validity is not strategy admissibility. ++ ++## Requirement Allocation ++ ++| Requirement/scenarios | Component allocation | ++| --- | --- | ++| REQ-independent-verification-008 digest mismatch | `bindings.js` before `selectStrategy`; facade test proves no verdict. | ++| REQ-independent-verification-005 unknown obligation/wrong node/partial `[A,B]` coverage | `evidence.js` binding metadata + `obligation-coverage.js` subset walk. | ++| REQ-independent-verification-006 aliasing and order adversaries | `strategy-policy.js`; `assessment.js` retains distinct tuple identities. | ++| REQ-kernel-contract-schemas-027 complete/missing/cross-family/frozen pins | assessment schema/fixtures, contract claims, K6b schema fixture tests. | ++| REQ-assurance-graph-007 contradiction/null digest | `projector.js` canonical binding validator before graph-id construction. | ++| REQ-assurance-graph-006 tampered/schema/candidate/policy/evidence/obligation/node replay | `assessment.js` validator + `assurance-graph/index.js` replay preflight. | ++| REQ-assurance-graph-008 nodes/canonical inputs/candidate/kind/schema/stored-id divergence | stored-payload graph-id recomputation and full canonical comparison in `assurance-graph/index.js`. | ++ ++## Testing Strategy ++ ++| Layer | What to Test | Approach | ++| --- | --- | --- | ++| Unit | Contract digest gate | C1 graph/C2 contract returns before strategy and emits no PASS. | ++| Unit | Strategy semantics | Same id for distinct roles; GREEN before RED; RED after PATCH; valid ordered distinct ids. | ++| Unit | Coverage and assessment identity | `[A,B]` with A only fails; multiple bindings union to `[A,B]`; coverage mutation changes id. | ++| Contract | `assessment/v1` | Valid complete/four-role fixtures; omitted coverage, verdict, and cross-family fail; frozen pins unchanged. | ++| Unit | Projector | Contradictory provided/authoritative digests and null resolved digest return `GRAPH_DIVERGENCE`. | ++| Integration | Replay | Tamper schema, id, candidate, policy, evidence reference, obligation, node, or coverage and fail closed. | ++| Integration | Reconcile | Tamper nodes, canonical inputs, candidate, kind/schema, or stored graph id and return `GRAPH_DIVERGENCE`. | ++| E2E | Full K6b pipeline | Valid token-complete evidence verifies, projects twice deterministically, replays byte-identically, and rejects stored mutations. | ++ ++## Migration / Rollout ++ ++No data migration or feature flag. Existing partial `assessment/v1` payloads without coverage are intentionally invalid and must be regenerated from canonical verifier inputs; they are not upgraded in place. Apply schema, verifier, projector/replay/reconcile, fixtures, tests, and temporary roadmap status as one rollback unit. Archive may restore K6b `done` and K6c `next-eligible` only after verification. Frozen `evidence/v2`, `verification/v2`, and K1 baselines must remain byte-identical throughout. ++ ++## Open Questions ++ ++None. +diff --git a/openspec/changes/k6b-semantic-integrity-remediation/proposal.md b/openspec/changes/k6b-semantic-integrity-remediation/proposal.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-semantic-integrity-remediation/proposal.md +@@ -0,0 +1,73 @@ ++# Proposal: k6b-semantic-integrity-remediation ++ ++## Intent ++ ++Cerrar seis defectos semánticos post-v2.51.0 (B1–B3, H1–H3) sin reabrir el macro-slice K6b ni iniciar K6c. Un Set de roles no equivale a evidencia no aliasable; `required_evidence.length > 0` no equivale a cobertura token a token; projector, replay y reconcile no fallan cerrados ante inputs contradictorios o payloads tampered. ++ ++## Scope ++ ++### In Scope ++- **B1 (corrección de REQ-006):** roles incompatibles MUST NOT compartir un EvidenceId; Strict TDD es RED → GREEN (no un Set); GREEN-before-RED y RED-after-PATCH fallan. `assessment_id` distintos por tupla `(evidence, role, obligation)`; la misma observación no satisface roles incompatibles. ++- **B2:** MUST-walk exige `required_evidence ⊆ evidence_requirements_satisfied` (como K4a). Persistir cobertura en assessment/binding (campo aditivo; nombre design-owned). ++- **B3:** Antes de strategy, `input.contract.contract_digest === executionGraph.contract_digest`; divergencia fail-closed. ++- **H1:** `projectAssuranceGraph` fail-closed si canonicalInputs contradicen Graph/contract/policy o si la preimage admite digests null. ++- **H2:** Replay revalida schema y recomputa `assessment_id`/`candidate_id`/`policy_snapshot_id`; evidencia, obligation, nodo implementador y `node_id`. ++- **H3:** Reconcile también nodes, canonical_inputs, candidate_id y kind/schema (o recompute `graph_id` del stored). ++- Tests adversariales. Docs: K6b `revise`; K6c `blocked-by-K6b-remediation` hasta archive. ++ ++### Out of Scope ++- Macro-slice K6b; K6c/K6d/K7/K8; PKI; mutar `evidence/v2` o K1 v1; rediseñar K6b; provenance v2.51.0. ++ ++## Capabilities ++ ++### New Capabilities ++- None. ++ ++### Modified Capabilities ++- `independent-verification`: REQ-006 deja de exigir relabeling de cuatro roles sobre el mismo EvidenceId; cobertura token a token; gate de digests contract ↔ Execution Graph antes de strategy. ++- `assurance-graph`: projector, replay y reconcile fail-closed (H1–H3). ++- `kernel-contract-schemas`: campo aditivo de cobertura en assessment/binding; pins `evidence/v2`, `verification/v2` y K1 v1 intactos. ++ ++## Approach ++ ++Corregir spec y runtime juntos. Strategy exige EvidenceIds distintos para roles incompatibles y orden temporal Strict TDD. MUST-walk consume cada token y persiste refs aditivas. Gate previo a strategy iguala contract digests. El projector público rechaza canonicalInputs contradictorios y preimages con digest null. Replay revalida assessments; reconcile compara el stored payload (o su `graph_id` recompute). Schema solo aditivo. ++ ++## Affected Areas ++ ++| Area | Impact | Description | ++|------|--------|-------------| ++| `scripts/lib/independent-verifier/` | Modified | B1–B3 + invertir REQ-006 | ++| `scripts/lib/assurance-graph/` | Modified | H1–H3 | ++| `schemas/kernel/assessment/` | Modified (additive) | Coverage field + fixtures | ++| `openspec/specs/{independent-verification,assurance-graph,kernel-contract-schemas}/` | Modified | Deltas | ++| `docs/{architecture,roadmaps}/harness-evolution.md` | Modified | K6b revise; K6c blocked | ++ ++## Risks ++ ++| Risk | Likelihood | Mitigation | ++|------|------------|------------| ++| Tests/spec v2.51.0 exigen aliasing de cuatro roles | High | Corrección conjunta spec+test+runtime | ++| Campo aditivo leído como mutación de `evidence/v2` | Med | Familia assessment/binding; pins byte-identical | ++| Docs dejan K6c `next-eligible` | Med | Roadmap en este change | ++ ++## Rollback Plan ++ ++Revertir runtime, schema aditivo, tests y docs como unidad. Conservar `evidence/v2`, `verification/v2` y pins K1. No migrar assessments parciales. K6b queda `revise` y K6c bloqueado hasta archive conforme. ++ ++## Dependencies ++ ++- Integridad K6b v2.51.0 (`2026-08-27-k6b-verification-integrity-remediation`); Obligation Manifest K4a; schemas `assessment/v1`, `evidence/v2`, `assurance-graph/v1`. Delivery: `exception-ok`. ++ ++## Success Criteria ++ ++- [ ] Mismo EvidenceId como RED+GREEN ⇒ FAIL. ++- [ ] GREEN before RED ⇒ FAIL. ++- [ ] RED after PATCH ⇒ FAIL. ++- [ ] `required_evidence = [A,B]` y solo A ⇒ FAIL. ++- [ ] Graph.contract=C1 y input.contract=C2 ⇒ FAIL. ++- [ ] Graph=C1 y canonicalInputs=C2 ⇒ `GRAPH_DIVERGENCE`. ++- [ ] `assessment_id` tampered ⇒ `GRAPH_DIVERGENCE` / invalid assessment. ++- [ ] Stored nodes tampered ⇒ `GRAPH_DIVERGENCE`. ++- [ ] Roadmap: K6b `revise`; K6c `blocked-by-K6b-remediation` hasta archive. ++ ++> **Branch advisory:** Before `sdd-apply` begins, a feature branch SHOULD be created following the `/` convention defined in the `branch-pr` skill (e.g. `git checkout -b feat/my-change main`). This note is SHOULD, not MUST — omit it from `status: blocked` envelopes. +diff --git a/openspec/changes/k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md b/openspec/changes/k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md +@@ -0,0 +1,95 @@ ++# Delta for Assurance Graph ++ ++## ADDED Requirements ++ ++### Requirement: Projector Fail-Closed On Contradictory Canonical Inputs {#REQ-assurance-graph-007} ++ ++`projectAssuranceGraph` MUST fail closed with `GRAPH_DIVERGENCE` when supplied ++`canonicalInputs` contradict the Graph, contract, or policy they claim to ++project. Required canonical input digests MUST be present and non-null. The ++projector MUST NOT fingerprint a null or absent digest for any required ++canonical input in the `graph_id` preimage. Contradiction or a null required ++digest MUST NOT yield a successful projection. ++ ++#### Scenario: Graph contract contradicts canonicalInputs ++ ++- GIVEN an Execution Graph whose contract digest is C1 ++- AND `canonicalInputs` whose contract digest is C2, where C1 ≠ C2 ++- WHEN `projectAssuranceGraph` runs ++- THEN projection MUST fail closed with `GRAPH_DIVERGENCE` ++- AND MUST NOT emit a graph whose `graph_id` fingerprints the contradictory pair ++ ++#### Scenario: Null required canonical digest is not fingerprinted ++ ++- GIVEN a required canonical input whose digest is null or absent ++- WHEN `projectAssuranceGraph` runs ++- THEN projection MUST fail closed ++- AND MUST NOT produce a `graph_id` that fingerprints a null digest for that input ++ ++### Requirement: Reconcile Stored Payload Divergence {#REQ-assurance-graph-008} ++ ++`reconcileAssuranceGraph` MUST detect stored-graph divergence beyond equality ++of `graph_id` and edges. It MUST fail closed with `GRAPH_DIVERGENCE` when ++stored `nodes`, `canonical_inputs`, `candidate_id`, or kind/schema diverge ++from the recomputed projection, or when `graph_id` recomputed from the stored ++payload does not match the stored `graph_id`. Comparing only stored ++`graph_id` plus edges MUST NOT be sufficient to declare reconciliation OK. ++ ++#### Scenario: Tampered stored nodes fail closed ++ ++- GIVEN a stored Assurance Graph whose `graph_id` and edges match a recomputation ++- AND whose persistable `nodes` have been altered ++- WHEN `reconcileAssuranceGraph` runs ++- THEN validation MUST fail closed with `GRAPH_DIVERGENCE` ++ ++#### Scenario: Stored payload or identity fields diverge ++ ++- GIVEN a stored Assurance Graph whose `candidate_id`, persistable `canonical_inputs`, or kind/schema differ from the current projection, or whose declared `graph_id` does not equal the digest recomputed from its stored payload ++- WHEN `reconcileAssuranceGraph` runs ++- THEN validation MUST fail closed with `GRAPH_DIVERGENCE` ++ ++## MODIFIED Requirements ++ ++### Requirement: Replay From Persistable Outputs {#REQ-assurance-graph-006} ++ ++The Assurance Graph MUST be reproducible from persistable outputs: canonical ++input digests, projected nodes and edges, evidence records, verification ++records, and assessment/binding records that carry `obligation_id`. Replay ++MUST NOT require ephemeral projector fields. Consumers MUST NOT reinvent ++`obligation_id` values from vanished fields. `satisfies` edges MUST be ++rebuildable from persistable assessments. `replayAssuranceGraph` MUST ++revalidate each persisted assessment before accepting the replay: schema ++validity; recomputed assessment identity; `candidate_id` match; bound ++policy-snapshot identity; referenced evidence exists; `obligation_id` is in ++the graph; the bound node implements that obligation; and persistable ++`node_id` matches the evidence record. Any check failure MUST fail closed ++with `GRAPH_DIVERGENCE` or as an invalid assessment. A tampered ++`assessment_id` MUST NOT replay as valid. ++(Previously: replay compared recomputed graph_id and edges without revalidating persisted assessments.) ++ ++#### Scenario: Replay from persisted outputs yields the same graph ++ ++- GIVEN a previously projected graph plus persisted evidence, verification, assessments, and canonical input digests ++- AND no ephemeral `obligation_ids` on the original projector call ++- AND every persisted assessment passes revalidation ++- WHEN the graph is replayed from those persistable outputs only ++- THEN the recomputed `graph_id` and edge set MUST be byte-identical to the stored graph ++ ++#### Scenario: Tampered assessment_id fails replay ++ ++- GIVEN persisted assessments plus a stored graph that would otherwise replay ++- AND one assessment whose `assessment_id` does not match the identity recomputed from its persistable fields ++- WHEN `replayAssuranceGraph` runs ++- THEN replay MUST fail closed with `GRAPH_DIVERGENCE` or as an invalid assessment ++ ++#### Scenario: Assessment fails schema, candidate, or policy revalidation ++ ++- GIVEN a persisted assessment that fails schema validation, whose `candidate_id` does not match the graph subject, or whose bound policy-snapshot identity does not match the graph ++- WHEN `replayAssuranceGraph` runs ++- THEN replay MUST fail closed with `GRAPH_DIVERGENCE` or as an invalid assessment ++ ++#### Scenario: Assessment bound to missing evidence or non-implementing node fails replay ++ ++- GIVEN a persisted assessment whose `evidence_id` does not exist, whose `obligation_id` is absent from the graph, whose node does not implement that obligation, or whose `node_id` disagrees with the evidence record ++- WHEN `replayAssuranceGraph` runs ++- THEN replay MUST fail closed with `GRAPH_DIVERGENCE` or as an invalid assessment +diff --git a/openspec/changes/k6b-semantic-integrity-remediation/specs/independent-verification/spec.md b/openspec/changes/k6b-semantic-integrity-remediation/specs/independent-verification/spec.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-semantic-integrity-remediation/specs/independent-verification/spec.md +@@ -0,0 +1,115 @@ ++# Delta for Independent Verification ++ ++## ADDED Requirements ++ ++### Requirement: Contract Digest Gate Before Strategy {#REQ-independent-verification-008} ++ ++Before strategy evaluation, `input.contract.contract_digest` MUST equal ++`executionGraph.contract_digest`. Mismatch MUST fail closed and MUST NOT ++proceed to strategy evaluation. ++ ++#### Scenario: Contract digest mismatch with Execution Graph fails closed before strategy ++ ++- GIVEN a frozen Candidate whose `input.contract.contract_digest` is C2 ++- AND an Execution Graph whose `contract_digest` is C1, where C1 ≠ C2 ++- WHEN the verifier validates canonical bindings ++- THEN verification MUST fail closed before strategy evaluation ++- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` ++ ++## MODIFIED Requirements ++ ++### Requirement: Obligation Manifest MUST Coverage {#REQ-independent-verification-005} ++ ++After strategy evaluation, the verifier MUST walk every Obligation Manifest ++item with criticality `must` that is not an approved `deferred` record ++(`reason` and `approved_by`). For each such obligation, a `PASS` or ++`PASS WITH WARNINGS` verdict MUST require `required_evidence` ⊆ persistable ++satisfied tokens, each persistably bound to that `obligation_id` and to a ++`node_id` listed in `implemented_by`. Coverage MUST be persisted on the ++assessment/binding record (additive field; exact name design-owned). A ++non-empty evidence list, unique-sort of `evidence_ids`, or existential ++binding MUST NOT substitute for that subset. Strategy role shape alone MUST ++NOT satisfy the graph. An `obligation_id` absent from the manifest MUST fail ++closed. Evidence bound to a node that does not implement the obligation MUST ++fail closed. A MUST whose `required_evidence` is not a subset of satisfied ++tokens MUST fail closed and MUST identify the unfulfilled `obligation_id`. ++The verifier MUST consume persistable manifest `obligation_id` values; it ++MUST NOT invent them from vanished fields. ++(Previously: coverage required admissible evidence "covering" required_evidence without subset semantics or persistable satisfied tokens.) ++ ++#### Scenario: MUST without admissible evidence fails closed ++ ++- GIVEN a compiled Execution Graph with a non-deferred MUST obligation ++- AND strategy role minimums met ++- AND no admissible evidence persistably bound to that `obligation_id` ++- WHEN the verifier evaluates obligation coverage ++- THEN it MUST fail closed identifying that `obligation_id` ++- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` ++ ++#### Scenario: Nonexistent obligation_id fails closed ++ ++- GIVEN an assessment or raw binding whose `obligation_id` is not in the Obligation Manifest ++- WHEN the verifier evaluates obligation coverage ++- THEN it MUST fail closed identifying the unknown `obligation_id` ++- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` ++ ++#### Scenario: Evidence bound to the wrong implementing node fails closed ++ ++- GIVEN a MUST obligation with `implemented_by` containing node A ++- AND admissible evidence whose persistable `node_id` is B, where B is not in `implemented_by` ++- WHEN the verifier evaluates that binding ++- THEN it MUST fail closed ++- AND MUST NOT treat the evidence as satisfying that obligation ++ ++#### Scenario: Partial required_evidence coverage fails closed ++ ++- GIVEN a non-deferred MUST obligation with `required_evidence` equal to `[A, B]` ++- AND admissible evidence that satisfies only token A ++- WHEN the verifier evaluates obligation coverage ++- THEN it MUST fail closed identifying that `obligation_id` ++- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` ++ ++### Requirement: Persistable Assessment Binding Distinct From Evidence {#REQ-independent-verification-006} ++ ++The verifier MUST persist an additive assessment/binding record for each ++evaluated tuple of `evidence_id`, `role`, `obligation_id`, `node_id`, and bound ++policy-snapshot identity. Assessment identity MUST include `role` and ++`obligation_id` and MUST be distinct per `(evidence_id, role, obligation_id)`. ++`evidence/v2` MUST remain the observation record and MUST NOT be mutated to ++carry `role` or `obligation_id`. Incompatible strategy roles MUST NOT share ++one EvidenceId. The same observation MUST NOT satisfy incompatible roles. ++Strict TDD evidence is a RED → GREEN sequence, not a set of role labels: ++GREEN-before-RED MUST fail closed, and RED-after-PATCH MUST fail closed. ++Unique-sort of `verification.evidence_ids` MUST NOT be the assessment identity ++and MUST NOT hide distinct role or obligation bindings. ++(Previously: the same EvidenceId used as four roles was required to yield four passing assessments.) ++ ++#### Scenario: Same EvidenceId as RED and GREEN fails closed ++ ++- GIVEN one `evidence/v2` observation whose `evidence_id` is E ++- AND that observation is bound as both RED and GREEN strategy roles ++- WHEN the verifier evaluates strategy evidence ++- THEN verification MUST fail closed ++- AND MUST NOT treat E as satisfying both roles ++ ++#### Scenario: GREEN before RED fails closed ++ ++- GIVEN Strict TDD or bug-strategy evidence whose GREEN observation precedes RED ++- WHEN the verifier evaluates strategy evidence ++- THEN verification MUST fail closed ++- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` ++ ++#### Scenario: RED after PATCH fails closed ++ ++- GIVEN Strict TDD or bug-strategy evidence whose RED observation occurs after PATCH ++- WHEN the verifier evaluates strategy evidence ++- THEN verification MUST fail closed ++- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` ++ ++#### Scenario: Distinct tuples yield distinct assessment identities ++ ++- GIVEN two persistable tuples that differ in `evidence_id`, `role`, or `obligation_id` ++- AND neither tuple shares one EvidenceId across incompatible roles ++- WHEN assessments are persisted ++- THEN their assessment identities MUST be distinct ++- AND unique-sort of `verification.evidence_ids` MUST NOT collapse those assessments +diff --git a/openspec/changes/k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md b/openspec/changes/k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md +@@ -0,0 +1,51 @@ ++# Delta for kernel-contract-schemas ++ ++## MODIFIED Requirements ++ ++### Requirement: Additive Assessment Binding Family Distinct From Evidence And Verification {#REQ-kernel-contract-schemas-027} ++ ++The suite MUST publish an additive assessment/binding schema family with a ++distinct `$id` and explicit `schema_version`. Exact `$id` is design-owned. ++Required persistable fields: assessment identity, `evidence_id`, `role`, ++`obligation_id`, `node_id`, `candidate_id` (`^sha256:[a-f0-9]{64}$`), bound ++policy-snapshot identity, and an additive coverage field recording the ++satisfied `required_evidence` tokens (exact field name design-owned). ++Assessment identity MUST incorporate `role` and `obligation_id`. The schema ++MUST enforce `additionalProperties: false` and MUST NOT include `verdict`. ++The family MUST NOT validate as `evidence/v2` or `verification/v2`. ++`evidence/v2`, `verification/v2`, and K1 v1 schema bytes and ++`K1_SCHEMA_BASELINE` pins MUST remain byte-identical. Valid and invalid ++fixtures MUST cover a complete binding including coverage, missing required ++fields, omitted coverage, and cross-family substitution. ++(Previously: assessment/binding required identity fields but no persistable coverage of satisfied required_evidence tokens.) ++ ++#### Scenario: Valid assessment fixture passes ++ ++- GIVEN a complete assessment/binding payload with role, obligation_id, node_id, evidence_id, policy-snapshot identity, and the additive coverage field ++- WHEN validated against the assessment/binding schema ++- THEN validation MUST succeed ++ ++#### Scenario: Cross-family substitution and verdict fail closed ++ ++- GIVEN an assessment payload validated as evidence/v2 or verification/v2, or an assessment payload that includes `verdict` ++- WHEN schema validation runs ++- THEN validation MUST fail closed identifying kind or required-field mismatch ++ ++#### Scenario: Four-role assessments remain distinct under the schema ++ ++- GIVEN four assessment payloads that share one `evidence_id` and differ only by `role` ++- WHEN each is validated and identities are compared ++- THEN all four MUST be schema-valid ++- AND their assessment identities MUST be pairwise distinct ++ ++#### Scenario: Assessment fixture without coverage field fails closed ++ ++- GIVEN an assessment/binding payload that omits the additive coverage field ++- WHEN validated against the assessment/binding schema ++- THEN validation MUST fail closed identifying the missing coverage field ++ ++#### Scenario: Evidence v2, verification v2, and K1 v1 pins remain frozen ++ ++- GIVEN `evidence/v2.schema.json`, `verification/v2.schema.json`, K1 v1 schemas, and `K1_SCHEMA_BASELINE` ++- WHEN verified after the additive coverage field is present on assessment/binding ++- THEN those schema and fixture bytes and K1 pins MUST remain byte-identical +diff --git a/openspec/changes/k6b-semantic-integrity-remediation/state.yaml b/openspec/changes/k6b-semantic-integrity-remediation/state.yaml +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-semantic-integrity-remediation/state.yaml +@@ -0,0 +1,170 @@ ++change: "k6b-semantic-integrity-remediation" ++status: "verified" ++last_updated: "2026-08-27T21:49:15Z" ++classification: high-risk ++blocking_questions: [] ++phases: ++ proposal: ++ status: done ++ artifact: "openspec/changes/k6b-semantic-integrity-remediation/proposal.md" ++ summary: "Focal B1–B3/H1–H3: REQ-006 aliasing, required_evidence subset, digest gate, projector/replay/reconcile fail-closed." ++ key_decisions: ++ - "REQ-006 is a spec correction: incompatible roles MUST NOT share one EvidenceId" ++ - "Coverage field is additive on assessment/binding; evidence/v2 and K1 v1 stay frozen" ++ - "Harden projectAssuranceGraph itself, not only verifyCandidate" ++ spec: ++ status: done ++ contract_remediation: resolved ++ artifacts: ++ - "openspec/changes/k6b-semantic-integrity-remediation/specs/independent-verification/spec.md" ++ - "openspec/changes/k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md" ++ - "openspec/changes/k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md" ++ summary: "Deltas B1–B3/H1–H3: invert REQ-006 aliasing; subset coverage; digest gate; projector/replay/reconcile fail-closed." ++ key_decisions: ++ - "REQ-006 inverted: incompatible roles MUST NOT share one EvidenceId; distinct ids per tuple remain" ++ - "Coverage field additive on assessment/binding; name design-owned; evidence/v2 and K1 v1 frozen" ++ - "H1–H3 bind projectAssuranceGraph, replayAssuranceGraph, and reconcileAssuranceGraph" ++ clarify: ++ status: skipped ++ rationale: "Validated sdd-spec signals reported residual_ambiguity=false with empty public_contract_questions, conflicting_requirements, and missing_acceptance_criteria arrays." ++ design: ++ status: done ++ artifact: "openspec/changes/k6b-semantic-integrity-remediation/design.md" ++ summary: "Token coverage on assessment/v1; ordered non-aliased roles; projector/replay/reconcile full fail-closed integrity." ++ key_decisions: ++ - "Require evidence_requirements_satisfied and include canonical coverage in assessment_id" ++ - "Use rawEvidence order and reject one EvidenceId across distinct strategy roles" ++ - "Validate canonical inputs, persisted assessments, and complete stored graph payloads" ++ tasks: ++ status: ready-for-apply ++ artifact: "openspec/changes/k6b-semantic-integrity-remediation/tasks.md" ++ summary: "Backlog TDD en cinco fases y 26 tareas: 22 históricas preservadas y 4 tareas de remediación runtime pendientes; delta estimado 120–220 líneas, total 970–1,370 con size-exception." ++ key_decisions: ++ - "Delivery strategy exception-ok; chain strategy size-exception" ++ - "Unidades ordenadas: contrato, verifier, Assurance Graph, integración/documentación" ++ - "Remediación focal: tareas 2.8, 3.7, 3.8 y 5.1 añaden únicamente evidencia runtime para los tres hallazgos tasks-gap" ++ apply: ++ status: done ++ artifact: "openspec/changes/k6b-semantic-integrity-remediation/apply-progress.md" ++ summary: "26/26 tareas verificadas: identidad de assessment y matrices runtime de replay/reconcile; sin cambio de producción." ++ key_decisions: ++ - "Legacy assessment/v1 sin cobertura requerida falla cerrada y se regenera; no hay migración" ++ - "models.yaml preexistente se preservó fuera de scope" ++ - "Las mutaciones de replay/reconcile recalculan identidades almacenadas para probar las ramas semánticas" ++ verify: ++ status: done ++ artifact: "openspec/changes/k6b-semantic-integrity-remediation/verify-report.md" ++ summary: "PASS: 22/22 MUST scenarios at runtime-test; 26/26 tasks evidenced; focused 82/82 and npm test 2762 pass, 0 fail." ++ key_decisions: ++ - "Prior tasks-gap branches now have persistent runtime matrices (2.8, 3.7, 3.8)" ++ - "Focused 82/82 and full npm test pass; frozen evidence/v2, verification/v2, K1 v1 intact" ++ - "Assumptions sdd-propose-001 and sdd-spec-001 remain confirmed" ++ archive: ++ status: pending ++ artifact: "openspec/changes/k6b-semantic-integrity-remediation/archive-report.md" ++ ++owner: ++ author: Manuel Retamozo ++ branch: fix/k6b-verification-integrity-remediation ++ ++route: ++ intended_route: standard ++ actual_route: standard ++ route_rationale: "First match: foundation/federated/bugfix/brownfield/refactor/hotfix conditions not met; project.status=active and classification=high-risk select standard (sdd-propose → sdd-spec → sdd-design → sdd-tasks → sdd-apply → sdd-verify → sdd-archive; gates: clarify, 4r-review-gate)." ++ validated: true ++ validation_errors: [] ++ ++approvals: ++ - id: intent-briefing-001 ++ gate: intent-briefing ++ decision: accepted ++ source: vscode/askQuestions ++ accepted_at: "2026-08-27T20:09:00Z" ++ synthesis: "Remediación focal post-v2.51.0 sobre verifier y Assurance Graph: roles incompatibles no pueden compartir EvidenceId; Strict TDD exige secuencia RED → GREEN (no un Set de etiquetas); cada token de required_evidence queda cubierto y persistido; input.contract.contract_digest === executionGraph.contract_digest antes de strategy; projector, replay y reconcile fallan cerrados ante inputs contradictorios, assessments tampered o stored graphs incompletos. Roadmap: K6b revise, K6c blocked. Fuera de alcance: K6c/K6d/K7/K8, PKI, rediseñar el macro-slice, mutar evidence/v2 o K1 v1 in-place." ++ scope: "In: integrity semántica de strategy (no aliasing de EvidenceId entre roles incompatibles; orden RED→GREEN / before→after); cobertura token a token de required_evidence persistida en el assessment/binding; binding contract_digest ↔ Execution Graph; projector fail-closed ante canonicalInputs contradictorios o digests ausentes; revalidación de assessments en replay; reconciliación completa del stored graph (nodes, edges, canonical inputs, candidate, kind/schema); tests adversariales; corrección de REQ-006; docs K6b revise / K6c blocked. Out: K6c challenges, K6d complexity, K7 review, K8 attestation, PKI, rediseño del macro-slice K6b, mutación in-place de evidence/v2 o contratos K1 v1." ++ applies_to: ++ - change-classification ++ - id: execution-mode-001 ++ gate: execution-mode ++ decision: interactive ++ source: vscode/askQuestions ++ accepted_at: "2026-08-27T20:26:00Z" ++ applies_to: ++ - sdd-propose ++ - sdd-spec ++ - sdd-design ++ - sdd-tasks ++ - sdd-apply ++ - sdd-verify ++ - sdd-archive ++ - id: delivery-strategy-001 ++ gate: delivery-strategy ++ decision: exception-ok ++ source: vscode/askQuestions ++ accepted_at: "2026-08-27T20:26:00Z" ++ applies_to: ++ - sdd-tasks ++ - sdd-apply ++ - id: architecture-001 ++ gate: architecture ++ decision: continue-to-sdd-design ++ source: vscode/askQuestions ++ accepted_at: "2026-08-27T20:48:37Z" ++ applies_to: ++ - sdd-design ++ - id: architecture-002 ++ gate: architecture ++ decision: continue-to-sdd-tasks ++ source: vscode/askQuestions ++ accepted_at: "2026-08-27T20:56:40Z" ++ applies_to: ++ - sdd-tasks ++ - id: review-workload-001 ++ gate: review-workload ++ decision: apply-with-size-exception ++ source: vscode/askQuestions ++ accepted_at: "2026-08-27T21:00:34Z" ++ applies_to: ++ - sdd-apply ++ - id: testing-001 ++ gate: testing ++ decision: continue-to-sdd-verify ++ source: vscode/askQuestions ++ accepted_at: "2026-08-27T21:20:56Z" ++ applies_to: ++ - sdd-verify ++ - id: testing-002 ++ gate: testing ++ decision: remediate-verify-tasks-gap ++ source: vscode/askQuestions ++ accepted_at: "2026-08-27T21:31:36Z" ++ applies_to: ++ - sdd-tasks ++ - id: review-workload-002 ++ gate: review-workload ++ decision: apply-remediation-with-size-exception ++ source: vscode/askQuestions ++ accepted_at: "2026-08-27T21:35:06Z" ++ applies_to: ++ - sdd-apply ++ ++assumptions: ++ - id: sdd-propose-001 ++ phase: sdd-propose ++ statement: "The persistable coverage field on assessment/binding is additive and design-owned (working name required_evidence_ref); evidence/v2 and K1 v1 stay frozen." ++ reversibility: high ++ basis: "User intent named required_evidence_ref as an example; predecessor treated assessment schema $id as design-owned; kernel-contract-schemas-027 already owns the additive family." ++ recorded_at: "2026-08-27T20:28:00Z" ++ status: confirmed ++ - id: sdd-spec-001 ++ phase: sdd-spec ++ statement: "The additive coverage field on assessment/binding is schema-required; omitting it MUST fail validation." ++ reversibility: high ++ basis: "Proposal requires persisted coverage on assessment/binding; exact field name remains design-owned." ++ recorded_at: "2026-08-27T20:40:00Z" ++ status: confirmed ++ ++baseline_fingerprints: ++ independent-verification: "sha256:3207ce8b5b280472b9b505378cf91df22b1ae22e007adb9dbf8a173405c5b0a0" ++ assurance-graph: "sha256:81f10f2046ae5519aead34f0f945275e99bfefbc03f9a898d80ce8bcbc437f56" ++ kernel-contract-schemas: "sha256:0f6c1f85aaff2f9b7a60320a66830bbc23feeb01a29ce4e6a8ef8f5d59ef4cec" +diff --git a/openspec/changes/k6b-semantic-integrity-remediation/tasks.md b/openspec/changes/k6b-semantic-integrity-remediation/tasks.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-semantic-integrity-remediation/tasks.md +@@ -0,0 +1,91 @@ ++# Tasks: k6b Semantic Integrity Remediation ++ ++## Spec/Design Reconciliation ++ ++| Requirement / Scenario | Priority | Design Allocation | Status | Notes | ++|---|---|---|---|---| ++| REQ-independent-verification-008 / contract digest mismatch | MUST | `scripts/lib/independent-verifier/bindings.js`, ordered facade in `index.js`; adversarial verifier test | covered-by-design | Gate runs before strategy and emits no verdict. | ++| REQ-independent-verification-005 / MUST token subset, unknown obligation, wrong node, partial coverage | MUST | `evidence.js`, `obligation-coverage.js`, `assessment.js`; coverage tests | covered-by-design | `required_evidence` is checked against sorted persisted satisfied tokens per binding. | ++| REQ-independent-verification-006 / distinct assessment bindings, no role aliasing, temporal order | MUST | `strategy-policy.js`, `assessment.js`, facade; strategy/identity tests | covered-by-design | Raw evidence order is authoritative; evidence/v2 remains unchanged. | ++| REQ-kernel-contract-schemas-027 / additive schema, coverage, closed family, frozen pins | MUST | `schemas/kernel/assessment/v1.schema.json`, `contract-claims.json`, fixtures and schema tests | covered-by-design | Includes complete, omitted-field, verdict/cross-family, four-role and pin checks. | ++| REQ-assurance-graph-007 / contradictory or null canonical inputs | MUST | `scripts/lib/assurance-graph/projector.js`; projector tests | covered-by-design | Required SHA-256 digests are validated before graph-id construction. | ++| REQ-assurance-graph-006 / replay assessment revalidation | MUST | `assessment.js`, `scripts/lib/assurance-graph/index.js`; replay integration tests | covered-by-design | Revalidates schema, identity, candidate/policy, evidence, obligation and node bindings. | ++| REQ-assurance-graph-008 / complete stored-payload reconciliation | MUST | `scripts/lib/assurance-graph/index.js`; reconcile tamper tests | covered-by-design | Recomputes stored graph id and compares nodes, canonical inputs and identity fields. | ++ ++### Reconciliation Verdict ++- MUST coverage: complete ++- SHOULD/MAY gaps: none ++- Ambiguities to track: none ++ ++## Review Workload Forecast ++ ++| Field | Value | ++|---|---| ++| Estimated changed lines | 970–1,370 (incremental remediation: 120–220) | ++| 400-line budget risk | High | ++| Chained PRs recommended | Yes | ++| Suggested split | Single maintainer-approved PR with explicit `size:exception`; implement in dependency-ordered work units. | ++| Delivery strategy | exception-ok | ++| Chain strategy | size-exception | ++ ++Decision needed before apply: No ++Chained PRs recommended: Yes ++Chain strategy: size-exception ++400-line budget risk: High ++ ++## Suggested Work Units ++ ++| Unit | Goal | Likely PR | Notes | ++|---|---|---|---| ++| 1 | Extend assessment contract and canonical identity | PR 1 (size exception) | Schema, claims, assessment id/validation, fixtures; verify frozen pins. | ++| 2 | Enforce verifier binding, strategy order and MUST token coverage | PR 1 (size exception) | RED tests first, then implementation and refactor; depends on Unit 1. | ++| 3 | Harden projector, replay and reconcile | PR 1 (size exception) | Canonical-input validation and full stored-payload checks; depends on Units 1–2. | ++| 4 | Integrate adversarial/e2e tests and roadmap status | PR 1 (size exception) | Run full `npm test`; docs remain within K6b scope. | ++| 5 | Close verify evidence gaps with persistent runtime tests | PR 1 (size exception) | Depends on existing verifier and Assurance Graph behavior; no production-code or contract changes. | ++ ++## Checklist Status Legend ++ ++- `[ ]` Not implemented yet ++- `[~]` Implemented but not yet verified locally ++- `[x]` Implemented and verified locally ++ ++## Phase 1: Assessment Contract and Fixtures ++ ++- [x] 1.1 Update `schemas/kernel/assessment/v1.schema.json` with required closed-array `evidence_requirements_satisfied`, preserving `additionalProperties: false` and excluding `verdict` [REQ-kernel-contract-schemas-027] ++- [x] 1.2 Add the additive assessment claim to `schemas/kernel/contract-claims.json` without changing its manifest `$id` or frozen K1 claims [REQ-kernel-contract-schemas-027] ++- [x] 1.3 Update/create `schemas/kernel/assessment/fixtures/valid/*.json` and `invalid/*.json` for complete coverage, omitted coverage, verdict/cross-family substitution, and four distinct roles [REQ-kernel-contract-schemas-027] ++- [x] 1.4 Write RED tests in `scripts/lib/k6b-schema-fixtures.test.js` for schema identity, required coverage, pairwise assessment ids, and unchanged evidence/v2, verification/v2, and K1 pins [REQ-kernel-contract-schemas-027] ++- [x] 1.5 Implement canonical assessment validation and `computeAssessmentId` coverage sorting/deduplication in `scripts/lib/independent-verifier/assessment.js`; GREEN then REFACTOR the tests from 1.4 [REQ-independent-verification-006, REQ-kernel-contract-schemas-027] ++ ++## Phase 2: Verifier Semantics and Coverage ++ ++- [x] 2.1 Add RED tests to `scripts/lib/independent-verifier/*.test.js` proving contract C1/C2 mismatch fails before strategy and produces no PASS [REQ-independent-verification-008] ++- [x] 2.2 Implement the canonical contract binding gate in `scripts/lib/independent-verifier/bindings.js` and ordered facade in `scripts/lib/independent-verifier/index.js`; GREEN and REFACTOR [REQ-independent-verification-008] ++- [x] 2.3 Add RED tests for same EvidenceId across incompatible roles, GREEN-before-RED, RED-after-PATCH, and valid ordered distinct ids [REQ-independent-verification-006] ++- [x] 2.4 Implement normalized role identity and temporal policy in `scripts/lib/independent-verifier/strategy-policy.js`, retaining rawEvidence order; GREEN and REFACTOR [REQ-independent-verification-006] ++- [x] 2.5 Add RED tests for unknown obligations, non-implementing nodes, no evidence, and `[A,B]` with only A satisfied [REQ-independent-verification-005] ++- [x] 2.6 Implement binding coverage normalization in `scripts/lib/independent-verifier/evidence.js` and token-subset MUST walk in `obligation-coverage.js`; persist coverage per assessment and GREEN/REFACTOR [REQ-independent-verification-005] ++- [x] 2.7 Update the verifier facade tests to prove gate order (bindings → strategy → MUST walk), distinct tuple assessments, and unchanged `evidence/v2`/`verification/v2` payload shape [REQ-independent-verification-005, REQ-independent-verification-006, REQ-independent-verification-008] ++- [x] 2.8 RED: extend `scripts/lib/independent-verifier/assessment.test.js` to vary `evidence_id` and `obligation_id` independently and assert distinct assessment identities for each tuple; GREEN/REFACTOR only if the focused runtime test exposes an implementation gap [REQ-independent-verification-006] ++ ++## Phase 3: Assurance Graph Integrity ++ ++- [x] 3.1 Add RED projector tests in `scripts/lib/assurance-graph/index.test.js` for contradictory Graph/contract/policy digests and null/absent required canonical digests [REQ-assurance-graph-007] ++- [x] 3.2 Implement strict resolved canonical-input validation in `scripts/lib/assurance-graph/projector.js` before graph-id preimage construction; GREEN and REFACTOR [REQ-assurance-graph-007] ++- [x] 3.3 Add RED replay cases for malformed/tampered assessment schema, assessment_id, candidate, policy, evidence reference, obligation, node, node_id and coverage [REQ-assurance-graph-006] ++- [x] 3.4 Implement persistable assessment revalidation and token-level coverage checks in `scripts/lib/assurance-graph/index.js`, using shared `assessment.js` helpers; GREEN and REFACTOR [REQ-assurance-graph-006] ++- [x] 3.5 Add RED reconcile cases for tampered nodes, canonical inputs, candidate_id, kind/schema, declared graph_id and stored payload identity [REQ-assurance-graph-008] ++- [x] 3.6 Implement stored-payload graph-id recomputation and full payload comparison in `scripts/lib/assurance-graph/index.js`; GREEN and REFACTOR [REQ-assurance-graph-008] ++- [x] 3.7 RED: extend `scripts/lib/assurance-graph/index.test.js` with persistent replay mutations for malformed assessment schema/coverage, candidate/policy mismatch, missing evidence, unknown obligation, non-implementing node, and `node_id` mismatch; GREEN/REFACTOR only if runtime tests expose an implementation gap [REQ-assurance-graph-006] ++- [x] 3.8 RED: extend `scripts/lib/assurance-graph/index.test.js` with persistent reconcile mutations for stored `canonical_inputs`, `candidate_id`, and kind/schema divergence, asserting `GRAPH_DIVERGENCE`; GREEN/REFACTOR only if runtime tests expose an implementation gap [REQ-assurance-graph-008] ++ ++## Phase 4: Integration, Documentation and Verification ++ ++- [x] 4.1 Extend `scripts/k6b-verifier-assurance-graph-e2e.test.js` with a valid token-complete verify/project/replay/reconcile path, deterministic second projection, and stored mutation failures [REQ-independent-verification-005, REQ-independent-verification-006, REQ-assurance-graph-006, REQ-assurance-graph-008] ++- [x] 4.2 Update `docs/architecture/harness-evolution.md` and `docs/roadmaps/harness-evolution.md` to mark K6b `revise` and K6c `blocked-by-K6b-remediation` until archive [REQ-assurance-graph-008] ++- [x] 4.3 Run `npm test` and inspect the complete native test output; resolve only failures attributable to the scoped B1–B3/H1–H3 remediation, preserving frozen K1 and v2 bytes [REQ-independent-verification-005, REQ-independent-verification-006, REQ-independent-verification-008, REQ-assurance-graph-006, REQ-assurance-graph-007, REQ-assurance-graph-008, REQ-kernel-contract-schemas-027] ++- [x] 4.4 Confirm no migration is attempted for legacy partial `assessment/v1` records and record regeneration/rollback behavior in apply progress [REQ-kernel-contract-schemas-027] ++ ++## Phase 5: Verify-Gap Runtime Evidence ++ ++- [x] 5.1 Run the focused verifier and Assurance Graph suites plus `npm test`; record the new runtime evidence and leave any implementation mismatch for the routed phase [REQ-independent-verification-006, REQ-assurance-graph-006, REQ-assurance-graph-008] +diff --git a/openspec/changes/k6b-semantic-integrity-remediation/verify-report.md b/openspec/changes/k6b-semantic-integrity-remediation/verify-report.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-semantic-integrity-remediation/verify-report.md +@@ -0,0 +1,153 @@ ++## Verification Report ++ ++**Change**: k6b-semantic-integrity-remediation ++**Version**: 2.51.0 ++**Mode**: Standard (TDD focused) ++ ++Re-verification after the prior FAIL (`origin: tasks-gap`). Authoritative checklist is now 26 tasks (22 historical + remediating 2.8, 3.7, 3.8, and 5.1). Lineage router (`getLineageNextAction`) returned `run-discovery` / `no-active-lineage`; this run executed the full discovery pipeline. No `verify_lineage` was opened because no BLOCKER/CRITICAL findings remain. ++ ++### Completeness ++ ++| Metric | Value | ++|--------|------:| ++| Tasks total | 26 | ++| Tasks complete | 26 | ++| Tasks incomplete | 0 | ++ ++All 26 checklist items in `tasks.md` are marked `[x]`. Independent runtime evidence now covers the three previously overclaimed tasks via 2.8, 3.7, and 3.8; task 5.1 records the focused plus full-suite execution. ++ ++### Build & Tests Execution ++ ++**Build**: N/A — `rules.verify.build_command` is empty and this CommonJS repository declares no independent type-check/build gate. ++ ++**Focused tests**: ✅ 82 passed / 0 failed / 0 skipped ++ ++```text ++node --test scripts/lib/k6b-schema-fixtures.test.js scripts/lib/independent-verifier/assessment.test.js scripts/lib/independent-verifier/obligation-coverage.test.js scripts/lib/independent-verifier/index.test.js scripts/lib/assurance-graph/index.test.js scripts/k6b-verifier-assurance-graph-e2e.test.js ++ ++exit code: 0 ++tests: 82 ++pass: 82 ++fail: 0 ++skipped: 0 ++duration_ms: 194.1959 ++``` ++ ++The prior FAIL recorded 79/79 on this command. The +3 tests are the persistent identity and adversarial matrices added by tasks 2.8, 3.7, and 3.8. ++ ++**Full regression suite**: ✅ 2762 passed / 0 failed / 2 skipped ++ ++```text ++npm test ++(raw: node --test scripts/**/*.test.js, then target generate/validate via scripts/check.js) ++ ++exit code: 0 ++Native Node tests: tests 2764, pass 2762, fail 0, skipped 2 ++terminal result: All checks passed. ++``` ++ ++**Static repository checks**: ✅ Passed ++ ++```text ++git diff --check ++exit code: 0 ++ ++git diff --quiet -- schemas/kernel/evidence schemas/kernel/verification scripts/lib/lifecycle-kernel/k1-compat.js ++exit code: 0 ++``` ++ ++Git emitted prospective LF/CRLF normalization notices for several modified files, but `git diff --check` found no whitespace error. Frozen `evidence/v2`, `verification/v2`, and K1 v1 bytes are unmodified. `models.yaml` has an independent pre-existing one-line model selection change and is excluded from this change's functional evidence. ++ ++**Manual verification**: source/test inspection performed; no separate UI or manual runtime workflow applies. ++ ++**Coverage**: ➖ Not available (`testing.coverage.available: false`). ++ ++**Quality gates**: no active `quality_gates:` policy is declared; Step 9a is a strict no-op and no `gates.quality-gates` audit block is written. ++ ++### Spec Compliance Matrix ++ ++| Requirement | Scenario | Evidence Level | Source | Result | Notes | ++|-------------|----------|----------------|--------|--------|-------| ++| REQ-assurance-graph-007 | Graph contract contradicts canonicalInputs | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > contradictory canonical inputs | PASS | Contract, policy, and Execution Graph digest contradictions return `GRAPH_DIVERGENCE`. | ++| REQ-assurance-graph-007 | Null required canonical digest is not fingerprinted | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > missing required canonical digest | PASS | Missing policy digest fails before graph-id construction. | ++| REQ-assurance-graph-008 | Tampered stored nodes fail closed | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > replay and reconcile tampering | PASS | Added stored node returns `GRAPH_DIVERGENCE`. | ++| REQ-assurance-graph-008 | Stored payload or identity fields diverge | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > stored identity mutations | PASS | Task 3.8 mutates stored `canonical_inputs`, `candidate_id`, `kind`, and `schema_version` after recomputing stored `graph_id`; each returns `GRAPH_DIVERGENCE`. | ++| REQ-assurance-graph-006 | Replay from persisted outputs yields the same graph | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` and K6b E2E | PASS | Replayed `graph_id` and edges are byte-identical. | ++| REQ-assurance-graph-006 | Tampered assessment_id fails replay | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > assessment tampering | PASS | Tampered identity returns `GRAPH_DIVERGENCE`. | ++| REQ-assurance-graph-006 | Assessment fails schema, candidate, or policy revalidation | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > replay binding mutations | PASS | Task 3.7 covers malformed schema, coverage outside requirements, candidate mismatch, and policy mismatch; each returns `GRAPH_DIVERGENCE`. | ++| REQ-assurance-graph-006 | Missing evidence or non-implementing node fails replay | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > replay binding mutations | PASS | Task 3.7 covers missing evidence, unknown obligation, non-implementing node, and `node_id` mismatch; each returns `GRAPH_DIVERGENCE`. | ++| REQ-independent-verification-008 | Contract digest mismatch fails before strategy | `runtime-test` | `scripts/lib/independent-verifier/index.test.js` > contract digest mismatch | PASS | Returns `BINDING_MISMATCH` with no verification verdict. | ++| REQ-independent-verification-005 | MUST without admissible evidence fails closed | `runtime-test` | verifier and obligation-coverage tests | PASS | Returns `UNFULFILLED_MUST` and identifies the obligation. | ++| REQ-independent-verification-005 | Nonexistent obligation_id fails closed | `runtime-test` | verifier and obligation-coverage tests | PASS | Returns `UNKNOWN_OBLIGATION_ID`. | ++| REQ-independent-verification-005 | Wrong implementing node fails closed | `runtime-test` | verifier and obligation-coverage tests | PASS | Returns `WRONG_IMPLEMENTING_NODE`. | ++| REQ-independent-verification-005 | Partial required_evidence coverage fails closed | `runtime-test` | `scripts/lib/independent-verifier/obligation-coverage.test.js` > token subset coverage | PASS | `[A,B]` with only A returns `UNFULFILLED_MUST`; complete union persists per-assessment coverage. | ++| REQ-independent-verification-006 | Same EvidenceId as RED and GREEN fails closed | `runtime-test` | `scripts/lib/independent-verifier/index.test.js` > incompatible role aliasing | PASS | Shared observation across distinct strategy roles returns `STRATEGY_EVIDENCE_ALIAS`. | ++| REQ-independent-verification-006 | GREEN before RED fails closed | `runtime-test` | `scripts/lib/independent-verifier/index.test.js` > strict-tdd role order | PASS | Returns `STRATEGY_SEQUENCE_VIOLATION`. | ++| REQ-independent-verification-006 | RED after PATCH fails closed | `runtime-test` | `scripts/lib/independent-verifier/index.test.js` > bug role order | PASS | Returns `STRATEGY_SEQUENCE_VIOLATION`. | ++| REQ-independent-verification-006 | Distinct tuples yield distinct assessment identities | `runtime-test` | `scripts/lib/independent-verifier/assessment.test.js` > independent `evidence_id`/`obligation_id` | PASS | Task 2.8 varies `evidence_id` and `obligation_id` independently (role held constant) and asserts three pairwise-distinct `assessment_id` values. | ++| REQ-kernel-contract-schemas-027 | Valid assessment fixture passes | `runtime-test` | `scripts/lib/k6b-schema-fixtures.test.js` | PASS | Complete coverage field validates. | ++| REQ-kernel-contract-schemas-027 | Cross-family substitution and verdict fail closed | `runtime-test` | `scripts/lib/k6b-schema-fixtures.test.js` | PASS | Assessment rejects verdict and does not validate as evidence/v2 or verification/v2. | ++| REQ-kernel-contract-schemas-027 | Four-role assessments remain distinct under schema | `runtime-test` | schema fixture and assessment unit tests | PASS | Four role-distinct records remain schema-valid and have pairwise assessment ids. | ++| REQ-kernel-contract-schemas-027 | Assessment without coverage fails closed | `runtime-test` | `scripts/lib/k6b-schema-fixtures.test.js` > missing coverage fixture | PASS | Omission of `evidence_requirements_satisfied` is rejected. | ++| REQ-kernel-contract-schemas-027 | Evidence v2, verification v2, and K1 v1 remain frozen | `runtime-test` + `static-proof` | K6b schema tests; `git diff --quiet` | PASS | Pin assertions pass and relevant schema/fixture/pin paths have no working-tree delta. | ++ ++**Compliance summary**: 22/22 scenarios satisfy the required evidence level (`runtime-test`, with frozen-pin `static-proof` on REQ-kernel-contract-schemas-027). ++ ++### Correctness (Static Evidence) ++ ++| Requirement | Status | Notes | ++|------------|--------|-------| ++| REQ-independent-verification-008 | ✅ Implemented | Contract digest equality is enforced in `validateBindings` before strategy selection. | ++| REQ-independent-verification-005 | ✅ Implemented | Required tokens are unioned per obligation and missing tokens fail closed. | ++| REQ-independent-verification-006 | ✅ Implemented | Role alias/order checks and tuple identity (role, `evidence_id`, `obligation_id`) are now proven at runtime. | ++| REQ-kernel-contract-schemas-027 | ✅ Implemented | Required closed coverage field, claims, fixtures, frozen families. | ++| REQ-assurance-graph-007 | ✅ Implemented | Required digests and contradictions fail before fingerprinting. | ++| REQ-assurance-graph-006 | ✅ Implemented | Replay revalidates schema, identity, candidate/policy, evidence, obligation, and node bindings. | ++| REQ-assurance-graph-008 | ✅ Implemented | Full stored-payload comparison includes nodes, canonical inputs, candidate, and kind/schema. | ++ ++### Coherence (Design) ++ ++| Decision | Followed? | Notes | ++|----------|-----------|-------| ++| ADR-001: required canonical coverage on assessment/v1 | ✅ Yes | Field is schema-required, canonicalized, and included in assessment identity; legacy omission fails closed. | ++| ADR-002: ordered, non-aliased strategy evidence | ✅ Yes | Raw order drives strict-tdd/bug sequencing and one id cannot cover distinct roles. | ++| ADR-003: canonical integrity across project/replay/reconcile | ✅ Yes | Shared helpers and fail-closed paths are now covered by persistent replay and reconcile adversarial matrices. | ++ ++No production-design deviation was found. Remediation tasks added tests only; apply progress records no production-code correction in batch 2. ++ ++### Traceability Matrix ++ ++| REQ | Tasks | Commits | Tests | Status | ++|-----|-------|---------|-------|--------| ++| REQ-independent-verification-008 | 2.1–2.2, 2.7, 4.3 | none (working tree) | verifier contract-digest test | OK | ++| REQ-independent-verification-005 | 2.5–2.7, 4.1, 4.3 | none (working tree) | verifier, obligation coverage, E2E | OK | ++| REQ-independent-verification-006 | 1.5, 2.3–2.4, 2.7, 2.8, 4.1, 4.3, 5.1 | none (working tree) | assessment identity + verifier strategy tests | OK | ++| REQ-kernel-contract-schemas-027 | 1.1–1.5, 4.3–4.4 | none (working tree) | K6b schema fixtures + assessment tests | OK | ++| REQ-assurance-graph-007 | 3.1–3.2, 4.3 | none (working tree) | projector tests | OK | ++| REQ-assurance-graph-006 | 3.3–3.4, 3.7, 4.1, 4.3, 5.1 | none (working tree) | replay tests + E2E | OK | ++| REQ-assurance-graph-008 | 3.5–3.6, 3.8, 4.1, 4.3, 5.1 | none (working tree) | reconcile tests + E2E | OK | ++ ++Traceability trailers are advisory because no active `traceability:` policy is declared. The absence of commits does not itself add a finding in this working-tree verification. ++ ++### Assumption Reconciliation ++ ++| id | statement | reversibility | outcome | ++|----|-----------|----------------|---------| ++| sdd-propose-001 | Coverage stays additive on assessment/binding; evidence/v2 and K1 v1 remain frozen. | high | confirmed | ++| sdd-spec-001 | Coverage is schema-required and omission fails validation. | high | confirmed | ++ ++### Issues Found ++ ++**CRITICAL**: None. ++ ++**WARNING**: None. ++ ++**SUGGESTION** ++ ++- Three historical BLOCKER entries for this change remain in `openspec/memory/known-issues.md` from the prior FAIL. Step 10b does not rewrite or close them on PASS; they are an audit trail of the remediated tasks-gap, not active verify findings. ++ ++### Verdict ++ ++**PASS** ++ ++All 26 tasks are complete, all 22 MUST scenarios now have `runtime-test` (or accepted frozen-pin `static-proof`) evidence, focused 82/82 and full `npm test` passed, and frozen evidence/v2, verification/v2, and K1 v1 bytes are unmodified. Route to `sdd-archive`. +diff --git a/openspec/memory/known-issues.md b/openspec/memory/known-issues.md +index afd74d0..7b91c78 100644 +--- a/openspec/memory/known-issues.md ++++ b/openspec/memory/known-issues.md +@@ -1,8 +1,29 @@ + --- + title: Known Issues +-last_updated: 2026-08-10 ++last_updated: 2026-08-27 + --- + ++## Distinct assessment tuple identity lacks evidence_id and obligation_id runtime coverage ++- severity: BLOCKER ++- area: scripts/lib/independent-verifier/assessment.test.js; scripts/lib/independent-verifier/index.test.js ++- workaround: add persistent tests that vary evidence_id and obligation_id independently, assert distinct assessment_id values, then rerun sdd-verify ++- change: k6b-semantic-integrity-remediation ++- date: 2026-08-27 ++ ++## Replay adversarial matrix is incomplete despite task 3.3 being marked complete ++- severity: BLOCKER ++- area: scripts/lib/assurance-graph/index.test.js ++- workaround: add replay cases for malformed schema or coverage, candidate and policy mismatch, missing evidence, unknown obligation, non-implementing node, and node_id mismatch; then rerun sdd-verify ++- change: k6b-semantic-integrity-remediation ++- date: 2026-08-27 ++ ++## Stored-payload reconciliation matrix omits canonical inputs, candidate, and kind/schema divergence ++- severity: BLOCKER ++- area: scripts/lib/assurance-graph/index.test.js ++- workaround: add persistent reconcile cases for stored canonical_inputs, candidate_id, and kind/schema divergence with complete payload identity checks; then rerun sdd-verify ++- change: k6b-semantic-integrity-remediation ++- date: 2026-08-27 ++ + ## npm test exit code 1: scripts/lib/verify-lineage.test.js crashes on load with Error: freezeCandidate requires diffText or diff_hash + - severity: BLOCKER + - area: scripts/lib/verify-lineage.test.js +diff --git a/schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json b/schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json +new file mode 100644 +--- /dev/null ++++ b/schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json +@@ -0,0 +1,11 @@ ++{ ++ "schema_version": 1, ++ "kind": "assessment/v1", ++ "assessment_id": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ++ "evidence_id": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", ++ "role": "acceptance", ++ "obligation_id": "req-repair-001", ++ "node_id": "repair-core", ++ "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", ++ "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ++} +diff --git a/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json b/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json +index 29caa79..db96f87 100644 +--- a/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json ++++ b/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json +@@ -8,5 +8,6 @@ + "node_id": "repair-core", + "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", ++ "evidence_requirements_satisfied": ["ev:test-pass"], + "verdict": "PASS" + } +diff --git a/schemas/kernel/assessment/fixtures/valid/v1-complete.json b/schemas/kernel/assessment/fixtures/valid/v1-complete.json +index 220455c..c7a494b 100644 +--- a/schemas/kernel/assessment/fixtures/valid/v1-complete.json ++++ b/schemas/kernel/assessment/fixtures/valid/v1-complete.json +@@ -7,5 +7,6 @@ + "obligation_id": "req-repair-001", + "node_id": "repair-core", + "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", +- "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ++ "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", ++ "evidence_requirements_satisfied": ["ev:test-pass"] + } +diff --git a/schemas/kernel/assessment/fixtures/valid/v1-four-roles.json b/schemas/kernel/assessment/fixtures/valid/v1-four-roles.json +index 66c16f7..bc428d3 100644 +--- a/schemas/kernel/assessment/fixtures/valid/v1-four-roles.json ++++ b/schemas/kernel/assessment/fixtures/valid/v1-four-roles.json +@@ -8,7 +8,8 @@ + "obligation_id": "req-repair-001", + "node_id": "repair-core", + "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", +- "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ++ "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", ++ "evidence_requirements_satisfied": ["ev:test-pass"] + }, + { + "schema_version": 1, +@@ -19,7 +20,8 @@ + "obligation_id": "req-repair-001", + "node_id": "repair-core", + "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", +- "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ++ "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", ++ "evidence_requirements_satisfied": ["ev:test-pass"] + }, + { + "schema_version": 1, +@@ -30,7 +32,8 @@ + "obligation_id": "req-repair-001", + "node_id": "repair-core", + "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", +- "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ++ "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", ++ "evidence_requirements_satisfied": ["ev:test-pass"] + }, + { + "schema_version": 1, +@@ -41,6 +44,7 @@ + "obligation_id": "req-repair-001", + "node_id": "repair-core", + "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", +- "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ++ "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", ++ "evidence_requirements_satisfied": ["ev:test-pass"] + } + ] +diff --git a/schemas/kernel/assessment/v1.schema.json b/schemas/kernel/assessment/v1.schema.json +index 831bfdf..a97ac5d 100644 +--- a/schemas/kernel/assessment/v1.schema.json ++++ b/schemas/kernel/assessment/v1.schema.json +@@ -14,7 +14,8 @@ + "obligation_id", + "node_id", + "candidate_id", +- "policy_snapshot_id" ++ "policy_snapshot_id", ++ "evidence_requirements_satisfied" + ], + "properties": { + "schema_version": { +@@ -52,6 +53,14 @@ + "policy_snapshot_id": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" ++ }, ++ "evidence_requirements_satisfied": { ++ "type": "array", ++ "items": { ++ "type": "string", ++ "minLength": 1 ++ }, ++ "uniqueItems": true + } + }, + "additionalProperties": false +diff --git a/schemas/kernel/contract-claims.json b/schemas/kernel/contract-claims.json +index 31d2ecc..076f2e9 100644 +--- a/schemas/kernel/contract-claims.json ++++ b/schemas/kernel/contract-claims.json +@@ -689,7 +689,8 @@ + "obligation_id", + "node_id", + "candidate_id", +- "policy_snapshot_id" ++ "policy_snapshot_id", ++ "evidence_requirements_satisfied" + ], + "enum_values": {}, + "command_shapes": [] +diff --git a/scripts/k6b-verifier-assurance-graph-e2e.test.js b/scripts/k6b-verifier-assurance-graph-e2e.test.js +index 5af0192..267c207 100644 +--- a/scripts/k6b-verifier-assurance-graph-e2e.test.js ++++ b/scripts/k6b-verifier-assurance-graph-e2e.test.js +@@ -44,10 +44,10 @@ const OBLIGATIONS = [ + + function featureEvidence() { + return [ +- { role: "acceptance", bytes: "acceptance ok", provenance: "runtime-observed", origin: "e2e-acceptance", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, +- { role: "invariants", bytes: "invariants ok", provenance: "runtime-observed", origin: "e2e-invariants", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, +- { role: "integration", bytes: "integration ok", provenance: "runtime-observed", origin: "e2e-integration", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, +- { role: "negative", bytes: "negative ok", provenance: "runtime-observed", origin: "e2e-negative", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, ++ { role: "acceptance", bytes: "acceptance ok", provenance: "runtime-observed", origin: "e2e-acceptance", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, ++ { role: "invariants", bytes: "invariants ok", provenance: "runtime-observed", origin: "e2e-invariants", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, ++ { role: "integration", bytes: "integration ok", provenance: "runtime-observed", origin: "e2e-integration", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, ++ { role: "negative", bytes: "negative ok", provenance: "runtime-observed", origin: "e2e-negative", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, + ]; + } + +diff --git a/scripts/lib/assurance-graph/index.js b/scripts/lib/assurance-graph/index.js +index 400d375..6af4c60 100644 +--- a/scripts/lib/assurance-graph/index.js ++++ b/scripts/lib/assurance-graph/index.js +@@ -1,7 +1,8 @@ + "use strict"; + +-const { projectAssuranceGraph, rejectForbidden } = require("./projector.js"); ++const { projectAssuranceGraph, rejectForbidden, canonicalize, computeGraphId } = require("./projector.js"); + const { computeInvalidationClosure, isEvidenceTransitivelyInvalidated } = require("./invalidation.js"); ++const { validateAssessment } = require("../independent-verifier/assessment.js"); + + function fail(reason_code, error) { + return { ok: false, reason_code, error: error || reason_code }; +@@ -17,19 +18,82 @@ function fail(reason_code, error) { + function reconcileAssuranceGraph(stored, canonicalInput) { + const projected = projectAssuranceGraph(canonicalInput); + if (!projected.ok) return projected; +- if (!stored || typeof stored !== "object" || stored.graph_id !== projected.graph.graph_id) { ++ if (!stored || typeof stored !== "object") { + return fail("GRAPH_DIVERGENCE", "stored Assurance Graph does not recompute from canonical inputs"); + } +- const storedEdges = JSON.stringify((stored.edges || []).map((e) => [e.from, e.relation, e.to]).sort()); +- const projectedEdges = JSON.stringify( +- projected.graph.edges.map((e) => [e.from, e.relation, e.to]).sort() +- ); +- if (storedEdges !== projectedEdges) { +- return fail("GRAPH_DIVERGENCE", "stored edges diverge from canonical projection"); ++ if (stored.kind !== "assurance-graph/v1" || stored.schema_version !== 1) { ++ return fail("GRAPH_DIVERGENCE", "stored graph kind or schema_version diverges"); ++ } ++ if (!Array.isArray(stored.nodes) || !Array.isArray(stored.edges) || !stored.canonical_inputs) { ++ return fail("GRAPH_DIVERGENCE", "stored graph payload is incomplete"); ++ } ++ const canonicalStored = canonicalize(stored.nodes, stored.edges); ++ const recomputedStoredId = computeGraphId({ ++ candidate_id: stored.candidate_id, ++ canonical_inputs: stored.canonical_inputs, ++ nodes: canonicalStored.nodes, ++ edges: canonicalStored.edges, ++ }); ++ if (stored.graph_id !== recomputedStoredId) { ++ return fail("GRAPH_DIVERGENCE", "stored graph_id does not match its stored payload"); ++ } ++ const fields = ["schema_version", "kind", "candidate_id", "graph_id", "nodes", "edges", "canonical_inputs"]; ++ for (const field of fields) { ++ if (JSON.stringify(stored[field]) !== JSON.stringify(projected.graph[field])) { ++ return fail("GRAPH_DIVERGENCE", `stored ${field} diverges from canonical projection`); ++ } + } + return { ok: true, graph: projected.graph }; + } + ++function isApprovedDeferred(obligation) { ++ return Boolean( ++ obligation && obligation.deferred && typeof obligation.deferred.reason === "string" && obligation.deferred.reason.trim() && ++ typeof obligation.deferred.approved_by === "string" && obligation.deferred.approved_by.trim() ++ ); ++} ++ ++function validateReplayAssessments(persistable) { ++ const assessments = Array.isArray(persistable.assessments) ? persistable.assessments : []; ++ const evidence = Array.isArray(persistable.evidence) ? persistable.evidence : []; ++ const graph = persistable.executionGraph; ++ const candidate = persistable.candidate; ++ if (!graph || !candidate || !Array.isArray(graph.obligations) || !Array.isArray(graph.nodes)) { ++ return fail("GRAPH_DIVERGENCE", "persistable graph, candidate, nodes, and obligations are required for replay"); ++ } ++ const evidenceById = new Map(evidence.map((record) => [record && record.evidence_id, record])); ++ const obligations = new Map(graph.obligations.map((obligation) => [obligation && obligation.id, obligation])); ++ const coveredByObligation = new Map(); ++ for (const assessment of assessments) { ++ const valid = validateAssessment(assessment); ++ if (!valid.ok) return fail("GRAPH_DIVERGENCE", valid.error); ++ const record = valid.assessment; ++ const obligation = obligations.get(record.obligation_id); ++ const evidenceRecord = evidenceById.get(record.evidence_id); ++ if (!obligation || !evidenceRecord || record.candidate_id !== candidate.candidate_id || ++ record.policy_snapshot_id !== graph.policy_snapshot_id || record.node_id !== evidenceRecord.node_id || ++ !Array.isArray(obligation.implemented_by) || !obligation.implemented_by.includes(record.node_id)) { ++ return fail("GRAPH_DIVERGENCE", "persisted assessment binding diverges from evidence, Candidate, policy, or obligation"); ++ } ++ const required = new Set(Array.isArray(obligation.required_evidence) ? obligation.required_evidence : []); ++ if (record.evidence_requirements_satisfied.some((token) => !required.has(token))) { ++ return fail("GRAPH_DIVERGENCE", "assessment coverage contains a token outside the obligation requirement"); ++ } ++ const coverage = coveredByObligation.get(record.obligation_id) || new Set(); ++ for (const token of record.evidence_requirements_satisfied) coverage.add(token); ++ coveredByObligation.set(record.obligation_id, coverage); ++ } ++ for (const obligation of graph.obligations) { ++ if (!obligation || String(obligation.criticality || "must").toLowerCase() !== "must" || isApprovedDeferred(obligation)) continue; ++ const required = Array.isArray(obligation.required_evidence) ? obligation.required_evidence : []; ++ const covered = coveredByObligation.get(obligation.id) || new Set(); ++ if (required.length === 0 || required.some((token) => !covered.has(token))) { ++ return fail("GRAPH_DIVERGENCE", `persisted assessments do not satisfy MUST obligation ${obligation.id}`); ++ } ++ } ++ return { ok: true }; ++} ++ + /** + * Replay a projection from persistable assessments, evidence, verification, and canonical_inputs. + * Never consumes ephemeral projector obligation_ids. +@@ -38,6 +102,8 @@ function reconcileAssuranceGraph(stored, canonicalInput) { + * @returns {{ ok: true, graph: object } | { ok: false, reason_code: string }} + */ + function replayAssuranceGraph(persistable = {}) { ++ const assessmentValidation = validateReplayAssessments(persistable); ++ if (!assessmentValidation.ok) return assessmentValidation; + return projectAssuranceGraph({ + canonicalInputs: persistable.canonical_inputs || persistable.canonicalInputs, + candidate: persistable.candidate, +@@ -76,6 +142,7 @@ module.exports = { + projectAssuranceGraph, + reconcileAssuranceGraph, + replayAssuranceGraph, ++ validateReplayAssessments, + rejectForbidden, + computeInvalidationClosure, + isEvidenceTransitivelyInvalidated, +diff --git a/scripts/lib/assurance-graph/index.test.js b/scripts/lib/assurance-graph/index.test.js +index b31bc70..77a0e1a 100644 +--- a/scripts/lib/assurance-graph/index.test.js ++++ b/scripts/lib/assurance-graph/index.test.js +@@ -17,6 +17,8 @@ const { + isEvidenceTransitivelyInvalidated, + } = require("./index.js"); + const { verifyCandidate } = require("../independent-verifier/index.js"); ++const { computeAssessmentId } = require("../independent-verifier/assessment.js"); ++const { canonicalize, computeGraphId } = require("./projector.js"); + + const SAMPLE_NODES = [ + { +@@ -76,10 +78,10 @@ function compileGraph() { + + function featureRaw() { + return [ +- { role: "acceptance", bytes: "acceptance", provenance: "runtime-observed", origin: "a", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, +- { role: "invariants", bytes: "invariants", provenance: "runtime-observed", origin: "i", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, +- { role: "contract", bytes: "contract", provenance: "runtime-observed", origin: "c", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, +- { role: "negative", bytes: "negative", provenance: "runtime-observed", origin: "n", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, ++ { role: "acceptance", bytes: "acceptance", provenance: "runtime-observed", origin: "a", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, ++ { role: "invariants", bytes: "invariants", provenance: "runtime-observed", origin: "i", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, ++ { role: "contract", bytes: "contract", provenance: "runtime-observed", origin: "c", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, ++ { role: "negative", bytes: "negative", provenance: "runtime-observed", origin: "n", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, + ]; + } + +@@ -92,6 +94,7 @@ function verifiedProjection() { + const verified = verifyCandidate({ + candidate, + executionGraph, ++ contract: { contract_digest: executionGraph.contract_digest }, + repository: { files }, + declaredStrategy: "feature", + collector: HARNESS_COLLECTOR, +@@ -101,6 +104,26 @@ function verifiedProjection() { + return { files, candidate, executionGraph, verified }; + } + ++function withAssessmentFields(assessment, fields) { ++ const updated = { ...assessment, ...fields }; ++ return { ...updated, assessment_id: computeAssessmentId(updated) }; ++} ++ ++function withStoredGraphId(stored) { ++ const canonical = canonicalize(stored.nodes, stored.edges); ++ return { ++ ...stored, ++ nodes: canonical.nodes, ++ edges: canonical.edges, ++ graph_id: computeGraphId({ ++ candidate_id: stored.candidate_id, ++ canonical_inputs: stored.canonical_inputs, ++ nodes: canonical.nodes, ++ edges: canonical.edges, ++ }), ++ }; ++} ++ + test("REQ-assurance-graph-002: same inputs yield the same digest and edges despite permutation", () => { + const { candidate, executionGraph, verified } = verifiedProjection(); + const classified = verified.evidence.map((evidence, index) => ({ +@@ -194,7 +217,7 @@ test("REQ-assurance-graph-001: matching canonical inputs project; divergence fai + ); + assert.equal(edgeDiverged.ok, false); + assert.equal(edgeDiverged.reason_code, "GRAPH_DIVERGENCE"); +- assert.match(edgeDiverged.error, /stored edges diverge from canonical projection/); ++ assert.match(edgeDiverged.error, /stored graph_id does not match its stored payload/); + }); + + test("REQ-harness-authority-canon-010: APIs return new objects without write-through", () => { +@@ -303,7 +326,7 @@ test("REQ-harness-authority-canon-010: graph used as approval or delivery author + assert.equal(result.reason_code, "GRAPH_AUTHORITY_MISUSE"); + }); + +-test("REQ-assurance-graph-002: graph_id changes when canonical inputs change; permutation does not", () => { ++test("REQ-assurance-graph-007: contradictory canonical inputs fail closed; permutation does not", () => { + const { candidate, executionGraph, verified } = verifiedProjection(); + const baseInput = { + candidate, +@@ -323,8 +346,8 @@ test("REQ-assurance-graph-002: graph_id changes when canonical inputs change; pe + contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + }); +- assert.equal(flippedContract.ok, true); +- assert.notEqual(base.graph.graph_id, flippedContract.graph.graph_id); ++ assert.equal(flippedContract.ok, false); ++ assert.equal(flippedContract.reason_code, "GRAPH_DIVERGENCE"); + + const flippedPolicy = projectAssuranceGraph({ + ...baseInput, +@@ -333,7 +356,8 @@ test("REQ-assurance-graph-002: graph_id changes when canonical inputs change; pe + policy_snapshot_id: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + }, + }); +- assert.notEqual(base.graph.graph_id, flippedPolicy.graph.graph_id); ++ assert.equal(flippedPolicy.ok, false); ++ assert.equal(flippedPolicy.reason_code, "GRAPH_DIVERGENCE"); + + const flippedExec = projectAssuranceGraph({ + ...baseInput, +@@ -342,7 +366,8 @@ test("REQ-assurance-graph-002: graph_id changes when canonical inputs change; pe + execution_graph_digest: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + }, + }); +- assert.notEqual(base.graph.graph_id, flippedExec.graph.graph_id); ++ assert.equal(flippedExec.ok, false); ++ assert.equal(flippedExec.reason_code, "GRAPH_DIVERGENCE"); + + const flippedOpenspec = projectAssuranceGraph({ + ...baseInput, +@@ -361,6 +386,15 @@ test("REQ-assurance-graph-002: graph_id changes when canonical inputs change; pe + assert.equal(base.graph.graph_id, permutedNodes.graph.graph_id); + }); + ++test("REQ-assurance-graph-007: missing required canonical digest is never fingerprinted", () => { ++ const { candidate, executionGraph } = verifiedProjection(); ++ const missingPolicy = { ...executionGraph }; ++ delete missingPolicy.policy_snapshot_id; ++ const result = projectAssuranceGraph({ candidate, executionGraph: missingPolicy }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "GRAPH_DIVERGENCE"); ++}); ++ + test("REQ-assurance-graph-005: rejectForbidden matches kind/namespace, not id substring", () => { + const allowed = rejectForbidden( + [{ id: "REQ-add-authorization-header", kind: "requirement" }], +@@ -412,7 +446,8 @@ test("REQ-assurance-graph-006: replay from persistable outputs is byte-identical + contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + }); +- assert.notEqual(churned.graph.graph_id, verified.assurance_graph.graph_id); ++ assert.equal(churned.ok, false); ++ assert.equal(churned.reason_code, "GRAPH_DIVERGENCE"); + const diverged = reconcileAssuranceGraph(verified.assurance_graph, { + candidate, + executionGraph, +@@ -428,3 +463,154 @@ test("REQ-assurance-graph-006: replay from persistable outputs is byte-identical + assert.equal(diverged.reason_code, "GRAPH_DIVERGENCE"); + }); + ++test("REQ-assurance-graph-006/008: replay and reconcile reject assessment and stored-payload tampering", () => { ++ const { candidate, executionGraph, verified } = verifiedProjection(); ++ const persistable = { ++ candidate, ++ executionGraph, ++ evidence: verified.evidence, ++ assessments: verified.assessments, ++ verification: verified.verification, ++ canonical_inputs: verified.assurance_graph.canonical_inputs, ++ }; ++ const tamperedAssessment = replayAssuranceGraph({ ++ ...persistable, ++ assessments: [{ ...verified.assessments[0], assessment_id: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" }, ...verified.assessments.slice(1)], ++ }); ++ assert.equal(tamperedAssessment.ok, false); ++ assert.equal(tamperedAssessment.reason_code, "GRAPH_DIVERGENCE"); ++ ++ const tamperedNode = reconcileAssuranceGraph( ++ { ...verified.assurance_graph, nodes: [...verified.assurance_graph.nodes, { id: "extra", kind: "source" }] }, ++ { candidate, executionGraph, evidence: verified.evidence, assessments: verified.assessments, verification: verified.verification } ++ ); ++ assert.equal(tamperedNode.ok, false); ++ assert.equal(tamperedNode.reason_code, "GRAPH_DIVERGENCE"); ++}); ++ ++test("REQ-assurance-graph-006: replay rejects every persisted assessment binding mutation", () => { ++ const { candidate, executionGraph, verified } = verifiedProjection(); ++ const persistable = { ++ candidate, ++ executionGraph, ++ evidence: verified.evidence, ++ assessments: verified.assessments, ++ verification: verified.verification, ++ canonical_inputs: verified.assurance_graph.canonical_inputs, ++ }; ++ const assessment = verified.assessments[0]; ++ const missingEvidenceId = "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; ++ ++ const cases = [ ++ { ++ name: "malformed assessment schema", ++ input: { ++ ...persistable, ++ assessments: [{ ...assessment, kind: "evidence/v2" }, ...verified.assessments.slice(1)], ++ }, ++ }, ++ { ++ name: "coverage outside the obligation requirements", ++ input: { ++ ...persistable, ++ assessments: [withAssessmentFields(assessment, { evidence_requirements_satisfied: ["ev:unexpected"] }), ...verified.assessments.slice(1)], ++ }, ++ }, ++ { ++ name: "candidate mismatch", ++ input: { ++ ...persistable, ++ assessments: [withAssessmentFields(assessment, { candidate_id: missingEvidenceId }), ...verified.assessments.slice(1)], ++ }, ++ }, ++ { ++ name: "policy mismatch", ++ input: { ++ ...persistable, ++ assessments: [withAssessmentFields(assessment, { policy_snapshot_id: missingEvidenceId }), ...verified.assessments.slice(1)], ++ }, ++ }, ++ { ++ name: "missing evidence", ++ input: { ++ ...persistable, ++ assessments: [withAssessmentFields(assessment, { evidence_id: missingEvidenceId }), ...verified.assessments.slice(1)], ++ }, ++ }, ++ { ++ name: "unknown obligation", ++ input: { ++ ...persistable, ++ assessments: [withAssessmentFields(assessment, { obligation_id: "req-unknown-001" }), ...verified.assessments.slice(1)], ++ }, ++ }, ++ { ++ name: "non-implementing node", ++ input: { ++ ...persistable, ++ evidence: [{ ...verified.evidence[0], node_id: "non-implementing-node" }, ...verified.evidence.slice(1)], ++ assessments: [withAssessmentFields(assessment, { node_id: "non-implementing-node" }), ...verified.assessments.slice(1)], ++ }, ++ }, ++ { ++ name: "node_id mismatch", ++ input: { ++ ...persistable, ++ evidence: [{ ...verified.evidence[0], node_id: "evidence-node-mismatch" }, ...verified.evidence.slice(1)], ++ }, ++ }, ++ ]; ++ ++ for (const { name, input } of cases) { ++ const replayed = replayAssuranceGraph(input); ++ assert.equal(replayed.ok, false, name); ++ assert.equal(replayed.reason_code, "GRAPH_DIVERGENCE", name); ++ } ++}); ++ ++test("REQ-assurance-graph-008: reconcile rejects stored identity mutations after recomputing stored graph_id", () => { ++ const { candidate, executionGraph, verified } = verifiedProjection(); ++ const canonicalInput = { ++ candidate, ++ executionGraph, ++ evidence: verified.evidence, ++ assessments: verified.assessments, ++ verification: verified.verification, ++ }; ++ const stored = verified.assurance_graph; ++ ++ const cases = [ ++ { ++ name: "canonical_inputs", ++ graph: withStoredGraphId({ ++ ...stored, ++ canonical_inputs: { ++ ...stored.canonical_inputs, ++ openspec_input_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", ++ }, ++ }), ++ }, ++ { ++ name: "candidate_id", ++ graph: withStoredGraphId({ ++ ...stored, ++ candidate_id: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", ++ }), ++ }, ++ { ++ name: "kind", ++ graph: withStoredGraphId({ ...stored, kind: "assurance-graph/v2" }), ++ }, ++ { ++ name: "schema_version", ++ graph: withStoredGraphId({ ...stored, schema_version: 2 }), ++ }, ++ ]; ++ ++ for (const { name, graph } of cases) { ++ const reconciled = reconcileAssuranceGraph(graph, canonicalInput); ++ assert.equal(reconciled.ok, false, name); ++ assert.equal(reconciled.reason_code, "GRAPH_DIVERGENCE", name); ++ } ++}); ++ +diff --git a/scripts/lib/assurance-graph/projector.js b/scripts/lib/assurance-graph/projector.js +index 40ed240..087918f 100644 +--- a/scripts/lib/assurance-graph/projector.js ++++ b/scripts/lib/assurance-graph/projector.js +@@ -102,6 +102,18 @@ function pushEdge(edges, from, relation, to) { + edges.push({ from, relation, to }); + } + ++function computeGraphId(payload) { ++ return sha256Fingerprint("assurance-graph/v1", { ++ candidate_id: payload.candidate_id, ++ contract_digest: payload.canonical_inputs.contract_digest, ++ policy_snapshot_id: payload.canonical_inputs.policy_snapshot_id, ++ execution_graph_digest: payload.canonical_inputs.execution_graph_digest, ++ openspec_input_digest: payload.canonical_inputs.openspec_input_digest, ++ nodes: payload.nodes, ++ edges: payload.edges, ++ }); ++} ++ + function resolveCanonicalInputDigests(input) { + const provided = input.canonicalInputs && typeof input.canonicalInputs === "object" ? input.canonicalInputs : {}; + const graph = input.executionGraph || {}; +@@ -117,22 +129,28 @@ function resolveCanonicalInputDigests(input) { + source_snapshot_id: graph.source_snapshot_id || (provided.sourceSnapshot && provided.sourceSnapshot.source_snapshot_id) || null, + }); + +- return { ++ const digests = { + contract_digest: contractDigest, + policy_snapshot_id: policySnapshotId, + execution_graph_digest: executionGraphDigest, + openspec_input_digest: openspecInputDigest, + }; +-} +- +-function persistableCanonicalInputs(digests) { +- const persistable = {}; +- for (const key of ["contract_digest", "policy_snapshot_id", "execution_graph_digest", "openspec_input_digest"]) { +- if (typeof digests[key] === "string" && SHA256.test(digests[key])) { +- persistable[key] = digests[key]; ++ for (const [key, value] of Object.entries(digests)) { ++ if (typeof value !== "string" || !SHA256.test(value)) { ++ return fail("GRAPH_DIVERGENCE", `canonical input ${key} must be a resolved sha256 digest`); + } + } +- return persistable; ++ const suppliedContract = provided.contract_digest || contract.contract_digest; ++ if (suppliedContract && suppliedContract !== graph.contract_digest) { ++ return fail("GRAPH_DIVERGENCE", "canonical contract digest contradicts Execution Graph"); ++ } ++ if (provided.policy_snapshot_id && provided.policy_snapshot_id !== graph.policy_snapshot_id) { ++ return fail("GRAPH_DIVERGENCE", "canonical policy snapshot contradicts Execution Graph"); ++ } ++ if (provided.execution_graph_digest && provided.execution_graph_digest !== graph.graph_id) { ++ return fail("GRAPH_DIVERGENCE", "canonical execution graph digest contradicts Execution Graph"); ++ } ++ return { ok: true, canonical_inputs: digests }; + } + + /** +@@ -208,13 +226,12 @@ function projectAssuranceGraph(input = {}) { + if (!forbidden.ok) return forbidden; + + const canonical = canonicalize(nodes, edges); +- const canonicalInputs = resolveCanonicalInputDigests(input); +- const graphId = sha256Fingerprint("assurance-graph/v1", { ++ const resolvedInputs = resolveCanonicalInputDigests(input); ++ if (!resolvedInputs.ok) return resolvedInputs; ++ const canonicalInputs = resolvedInputs.canonical_inputs; ++ const graphId = computeGraphId({ + candidate_id: candidateId, +- contract_digest: canonicalInputs.contract_digest, +- policy_snapshot_id: canonicalInputs.policy_snapshot_id, +- execution_graph_digest: canonicalInputs.execution_graph_digest, +- openspec_input_digest: canonicalInputs.openspec_input_digest, ++ canonical_inputs: canonicalInputs, + nodes: canonical.nodes, + edges: canonical.edges, + }); +@@ -227,10 +244,7 @@ function projectAssuranceGraph(input = {}) { + nodes: canonical.nodes.map(cloneNode), + edges: canonical.edges.map(cloneEdge), + }; +- const persistedInputs = persistableCanonicalInputs(canonicalInputs); +- if (Object.keys(persistedInputs).length > 0) { +- resultGraph.canonical_inputs = persistedInputs; +- } ++ resultGraph.canonical_inputs = canonicalInputs; + + return { ok: true, graph: resultGraph }; + } +@@ -238,6 +252,8 @@ function projectAssuranceGraph(input = {}) { + module.exports = { + ALLOWED_RELATIONS, + canonicalize, ++ computeGraphId, ++ resolveCanonicalInputDigests, + rejectForbidden, + projectAssuranceGraph, + }; +diff --git a/scripts/lib/independent-verifier/assessment.js b/scripts/lib/independent-verifier/assessment.js +index 8910bf8..25527b2 100644 +--- a/scripts/lib/independent-verifier/assessment.js ++++ b/scripts/lib/independent-verifier/assessment.js +@@ -21,7 +21,15 @@ function getAssessmentSchema() { + return cachedAssessmentSchema; + } + ++function canonicalizeEvidenceRequirements(value) { ++ if (!Array.isArray(value) || value.some((token) => typeof token !== "string" || token.length === 0)) { ++ return null; ++ } ++ return [...new Set(value)].sort(); ++} ++ + function computeAssessmentId(fields) { ++ const coverage = canonicalizeEvidenceRequirements(fields.evidence_requirements_satisfied) || []; + return sha256Fingerprint("assessment/v1", { + schema_version: fields.schema_version, + kind: fields.kind, +@@ -31,9 +39,33 @@ function computeAssessmentId(fields) { + node_id: fields.node_id, + candidate_id: fields.candidate_id, + policy_snapshot_id: fields.policy_snapshot_id, ++ evidence_requirements_satisfied: coverage, + }); + } + ++function validateAssessment(record) { ++ if (!record || typeof record !== "object") { ++ return fail("INVALID_ASSESSMENT", "assessment must be an object"); ++ } ++ if (Object.prototype.hasOwnProperty.call(record, "verdict")) { ++ return fail("MIXED_ASSESSMENT_VERDICT", "assessment must not carry verdict"); ++ } ++ const coverage = canonicalizeEvidenceRequirements(record.evidence_requirements_satisfied); ++ if (!coverage) { ++ return fail("INVALID_ASSESSMENT", "evidence_requirements_satisfied must be a string array"); ++ } ++ const normalized = { ...record, evidence_requirements_satisfied: coverage }; ++ const validation = validateInstance(getAssessmentSchema(), normalized); ++ if (!validation.valid) { ++ return fail("INVALID_ASSESSMENT", validation.errors.map((e) => e.message).join("; ")); ++ } ++ const expectedId = computeAssessmentId(normalized); ++ if (normalized.assessment_id !== expectedId) { ++ return fail("INVALID_ASSESSMENT", "assessment_id does not match canonical assessment fields"); ++ } ++ return { ok: true, assessment: normalized }; ++} ++ + /** + * Emit an assessment/v1 binding. Verdict is forbidden. + * +@@ -57,21 +89,23 @@ function emitAssessment(input) { + node_id: input.node_id, + candidate_id: input.candidate_id, + policy_snapshot_id: input.policy_snapshot_id, ++ evidence_requirements_satisfied: canonicalizeEvidenceRequirements(input.evidence_requirements_satisfied), + }; ++ if (!record.evidence_requirements_satisfied) { ++ return fail("INVALID_ASSESSMENT", "evidence_requirements_satisfied must be a string array"); ++ } + record.assessment_id = computeAssessmentId(record); + + if (!SHA256.test(record.assessment_id) || !SHA256.test(record.evidence_id) || !SHA256.test(record.candidate_id)) { + return fail("INVALID_ASSESSMENT", "assessment digests must be sha256"); + } + +- const validation = validateInstance(getAssessmentSchema(), record); +- if (!validation.valid) { +- return fail("INVALID_ASSESSMENT", validation.errors.map((e) => e.message).join("; ")); +- } +- return { ok: true, assessment: record }; ++ return validateAssessment(record); + } + + module.exports = { ++ canonicalizeEvidenceRequirements, + computeAssessmentId, + emitAssessment, ++ validateAssessment, + }; +diff --git a/scripts/lib/independent-verifier/assessment.test.js b/scripts/lib/independent-verifier/assessment.test.js +index 601fd96..2ab8ecc 100644 +--- a/scripts/lib/independent-verifier/assessment.test.js ++++ b/scripts/lib/independent-verifier/assessment.test.js +@@ -13,13 +13,36 @@ const BASE = { + node_id: "repair-core", + candidate_id: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + policy_snapshot_id: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", ++ evidence_requirements_satisfied: ["ev:test-pass"], + }; + +-test("REQ-independent-verification-006: computeAssessmentId includes role and obligation_id", () => { ++test("REQ-independent-verification-006: computeAssessmentId includes role, obligation_id, and canonical coverage", () => { + const a = computeAssessmentId({ ...BASE, role: "acceptance" }); + const b = computeAssessmentId({ ...BASE, role: "invariants" }); + assert.match(a, /^sha256:[a-f0-9]{64}$/); + assert.notEqual(a, b); ++ assert.equal( ++ computeAssessmentId({ ...BASE, role: "acceptance", evidence_requirements_satisfied: ["b", "a", "a"] }), ++ computeAssessmentId({ ...BASE, role: "acceptance", evidence_requirements_satisfied: ["a", "b"] }) ++ ); ++}); ++ ++test("REQ-independent-verification-006: evidence_id and obligation_id independently change assessment identity", () => { ++ const baseline = computeAssessmentId({ ...BASE, role: "acceptance" }); ++ const differentEvidence = computeAssessmentId({ ++ ...BASE, ++ role: "acceptance", ++ evidence_id: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", ++ }); ++ const differentObligation = computeAssessmentId({ ++ ...BASE, ++ role: "acceptance", ++ obligation_id: "req-repair-002", ++ }); ++ ++ assert.notEqual(baseline, differentEvidence); ++ assert.notEqual(baseline, differentObligation); ++ assert.notEqual(differentEvidence, differentObligation); + }); + + test("REQ-independent-verification-006: emitAssessment validates and rejects verdict", () => { +diff --git a/scripts/lib/independent-verifier/bindings.js b/scripts/lib/independent-verifier/bindings.js +index b6ac476..e8d856a 100644 +--- a/scripts/lib/independent-verifier/bindings.js ++++ b/scripts/lib/independent-verifier/bindings.js +@@ -74,6 +74,14 @@ function validateBindings(input) { + return fail("BINDING_MISMATCH", graphBinding.error || graphBinding.reason_code); + } + ++ const contract = input.contract; ++ if (!contract || typeof contract !== "object" || typeof contract.contract_digest !== "string") { ++ return fail("BINDING_MISMATCH", "input.contract.contract_digest is required"); ++ } ++ if (contract.contract_digest !== executionGraph.contract_digest) { ++ return fail("BINDING_MISMATCH", "input.contract.contract_digest does not match executionGraph.contract_digest"); ++ } ++ + const repository = input.repository; + if (!repository || typeof repository !== "object" || !repository.files) { + return fail("BINDING_MISMATCH", "repository bytes are required"); +@@ -88,7 +96,7 @@ function validateBindings(input) { + return fail("BINDING_MISMATCH", "repository tree does not match candidate_tree"); + } + +- return { ok: true, candidate, executionGraph }; ++ return { ok: true, candidate, executionGraph, contract }; + } + + module.exports = { +diff --git a/scripts/lib/independent-verifier/evidence.js b/scripts/lib/independent-verifier/evidence.js +index 9f1aff1..6e444bc 100644 +--- a/scripts/lib/independent-verifier/evidence.js ++++ b/scripts/lib/independent-verifier/evidence.js +@@ -116,12 +116,16 @@ function normalizeEvidence(raw, candidate, executionGraph, harnessCollector) { + : raw.obligation_id + ? [raw.obligation_id] + : []; ++ const evidenceRequirementsSatisfied = Array.isArray(raw.evidence_requirements_satisfied) ++ ? [...new Set(raw.evidence_requirements_satisfied.filter((token) => typeof token === "string" && token.length > 0))].sort() ++ : []; + + return { + ok: true, + evidence: record, + role: raw.role, + obligation_ids: obligationIds, ++ evidence_requirements_satisfied: evidenceRequirementsSatisfied, + }; + } + +diff --git a/scripts/lib/independent-verifier/index.test.js b/scripts/lib/independent-verifier/index.test.js +index 2b816e3..847ca46 100644 +--- a/scripts/lib/independent-verifier/index.test.js ++++ b/scripts/lib/independent-verifier/index.test.js +@@ -103,6 +103,7 @@ function raw(role, bytes, extra = {}) { + origin: extra.origin || `role:${role}`, + node_id: extra.node_id || "repair-core", + obligation_ids: extra.obligation_ids || ["req-repair-001"], ++ evidence_requirements_satisfied: extra.evidence_requirements_satisfied || ["ev:test-pass"], + }; + if (Object.prototype.hasOwnProperty.call(extra, "collector") && extra.collector) { + record.collector = extra.collector; +@@ -181,6 +182,20 @@ test("REQ-independent-verification-001: binding digest mismatch fails closed", ( + assert.equal(result.reason_code, "BINDING_MISMATCH"); + }); + ++test("REQ-independent-verification-008: contract digest mismatch fails before strategy or verdict", () => { ++ const harness = buildHarness(); ++ const result = verifyCandidate({ ++ ...harness, ++ contract: { ...harness.contract, contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" }, ++ declaredStrategy: "feature", ++ rawEvidence: [], ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "BINDING_MISMATCH"); ++ assert.equal(Object.prototype.hasOwnProperty.call(result, "verification"), false); ++ assert.doesNotMatch(result.error, /strategy|MUST/i); ++}); ++ + test("REQ-independent-verification-001: repository tree_digest without bytes is rejected", () => { + const harness = buildHarness(); + const result = verifyCandidate({ +@@ -767,7 +782,7 @@ test("REQ-independent-verification-004: strategy failure short-circuits without + assert.notEqual(result.reason_code, "UNFULFILLED_MUST"); + }); + +-test("REQ-independent-verification-006: four roles over one observation yield four assessments and one evidence_id", () => { ++test("REQ-independent-verification-006: one observation cannot satisfy four incompatible roles", () => { + const harness = buildHarness(); + const shared = { origin: "shared-observation", obligation_ids: ["req-repair-001"] }; + const bytes = "shared-bytes"; +@@ -781,29 +796,27 @@ test("REQ-independent-verification-006: four roles over one observation yield fo + raw("negative", bytes, shared), + ], + }); +- assert.equal(result.ok, true, result.error || result.reason_code); +- assert.equal(result.assessments.length, 4); +- const evidenceIds = new Set(result.evidence.map((ev) => ev.evidence_id)); +- assert.equal(evidenceIds.size, 1); +- assert.equal(new Set(result.verification.evidence_ids).size, 1); +- const assessmentIds = new Set(result.assessments.map((a) => a.assessment_id)); +- assert.equal(assessmentIds.size, 4); +- const roles = new Set(result.assessments.map((a) => a.role)); +- assert.equal(roles.size, 4); +- const sample = result.assessments[0]; +- assert.equal( +- sample.assessment_id, +- computeAssessmentId({ +- schema_version: 1, +- kind: "assessment/v1", +- evidence_id: sample.evidence_id, +- role: sample.role, +- obligation_id: sample.obligation_id, +- node_id: sample.node_id, +- candidate_id: sample.candidate_id, +- policy_snapshot_id: sample.policy_snapshot_id, +- }) +- ); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "STRATEGY_EVIDENCE_ALIAS"); ++ assert.equal(Object.prototype.hasOwnProperty.call(result, "verification"), false); ++}); ++ ++test("REQ-independent-verification-006: strict-tdd and bug role order fail closed", () => { ++ const harness = buildHarness(); ++ const strict = verifyCandidate({ ++ ...harness, ++ rawEvidence: [raw("green", "green"), raw("red", "red")], ++ }); ++ assert.equal(strict.ok, false); ++ assert.equal(strict.reason_code, "STRATEGY_SEQUENCE_VIOLATION"); ++ ++ const bug = verifyCandidate({ ++ ...harness, ++ declaredStrategy: "bug", ++ rawEvidence: [raw("patch", "patch"), raw("red", "red"), raw("green", "green")], ++ }); ++ assert.equal(bug.ok, false); ++ assert.equal(bug.reason_code, "STRATEGY_SEQUENCE_VIOLATION"); + }); + + test("FABRICATED_EVIDENCE: non-object raw and missing origin fail closed", () => { +diff --git a/scripts/lib/independent-verifier/obligation-coverage.js b/scripts/lib/independent-verifier/obligation-coverage.js +index d57a275..ac0cd18 100644 +--- a/scripts/lib/independent-verifier/obligation-coverage.js ++++ b/scripts/lib/independent-verifier/obligation-coverage.js +@@ -23,6 +23,15 @@ function isMust(obligation) { + return String((obligation && obligation.criticality) || "must").toLowerCase() === "must"; + } + ++function normalizedCoverage(item, requiredEvidence) { ++ const supplied = item && (item.evidence_requirements_satisfied || (item.evidence && item.evidence.evidence_requirements_satisfied)); ++ const satisfied = Array.isArray(supplied) ++ ? supplied.filter((token) => typeof token === "string") ++ : []; ++ const required = new Set(requiredEvidence); ++ return [...new Set(satisfied.filter((token) => required.has(token)))].sort(); ++} ++ + /** + * Walk non-deferred MUST obligations after strategy evaluation. + * Join key is persistable obligation_id, not K4a evidence tokens. +@@ -96,6 +105,15 @@ function walkMustObligations(input) { + return fail("UNFULFILLED_MUST", `MUST obligation ${obligation.id} has no admissible evidence`); + } + ++ const satisfiedTokens = new Set(); ++ for (const item of admissible) { ++ for (const token of normalizedCoverage(item, requiredEvidence)) satisfiedTokens.add(token); ++ } ++ const missingTokens = requiredEvidence.filter((token) => !satisfiedTokens.has(token)); ++ if (missingTokens.length > 0) { ++ return fail("UNFULFILLED_MUST", `MUST obligation ${obligation.id} is missing required evidence: ${missingTokens.join(", ")}`); ++ } ++ + for (const item of admissible) { + const emitted = emitAssessment({ + evidence_id: item.evidence.evidence_id, +@@ -104,6 +122,7 @@ function walkMustObligations(input) { + node_id: item.evidence.node_id, + candidate_id: candidate && candidate.candidate_id, + policy_snapshot_id: policySnapshotId, ++ evidence_requirements_satisfied: normalizedCoverage(item, requiredEvidence), + }); + if (!emitted.ok) return emitted; + assessments.push(emitted.assessment); +@@ -115,5 +134,6 @@ function walkMustObligations(input) { + + module.exports = { + isApprovedDeferred, ++ normalizedCoverage, + walkMustObligations, + }; +diff --git a/scripts/lib/independent-verifier/obligation-coverage.test.js b/scripts/lib/independent-verifier/obligation-coverage.test.js +index df1be52..05a785e 100644 +--- a/scripts/lib/independent-verifier/obligation-coverage.test.js ++++ b/scripts/lib/independent-verifier/obligation-coverage.test.js +@@ -11,6 +11,7 @@ const EVIDENCE = { + evidence_id: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + provenance: "runtime-observed", + node_id: "repair-core", ++ evidence_requirements_satisfied: ["ev:test-pass"], + }; + + function graph(obligations) { +@@ -141,6 +142,31 @@ test("REQ-independent-verification-005: strategy-shaped bindings still emit pers + assert.equal(result.assessments[0].role, "acceptance"); + }); + ++test("REQ-independent-verification-005: token subset coverage rejects partial bindings and persists the complete union", () => { ++ const base = { ++ id: "req-repair-001", ++ criticality: "must", ++ implemented_by: ["repair-core"], ++ required_evidence: ["ev:a", "ev:b"], ++ }; ++ const partial = walkMustObligations({ ++ classified: [{ role: "acceptance", evidence: EVIDENCE, obligation_ids: [base.id], evidence_requirements_satisfied: ["ev:a"] }], ++ executionGraph: graph([base]), candidate: CANDIDATE, policySnapshotId: POLICY, ++ }); ++ assert.equal(partial.ok, false); ++ assert.equal(partial.reason_code, "UNFULFILLED_MUST"); ++ ++ const complete = walkMustObligations({ ++ classified: [ ++ { role: "acceptance", evidence: EVIDENCE, obligation_ids: [base.id], evidence_requirements_satisfied: ["ev:b"] }, ++ { role: "invariants", evidence: { ...EVIDENCE, evidence_id: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, obligation_ids: [base.id], evidence_requirements_satisfied: ["ev:a"] }, ++ ], ++ executionGraph: graph([base]), candidate: CANDIDATE, policySnapshotId: POLICY, ++ }); ++ assert.equal(complete.ok, true, complete.error); ++ assert.deepEqual(complete.assessments.map((assessment) => assessment.evidence_requirements_satisfied), [["ev:b"], ["ev:a"]]); ++}); ++ + test("REQ-independent-verification-005: weak provenance on MUST is INSUFFICIENT_PROVENANCE", () => { + const runtimeUnbound = { + ...EVIDENCE, +diff --git a/scripts/lib/independent-verifier/strategy-policy.js b/scripts/lib/independent-verifier/strategy-policy.js +index b060db6..f83de83 100644 +--- a/scripts/lib/independent-verifier/strategy-policy.js ++++ b/scripts/lib/independent-verifier/strategy-policy.js +@@ -114,6 +114,39 @@ function failIfInadmissible(policy, role, items, message) { + return null; + } + ++function assertDistinctRoleEvidence(items) { ++ const rolesByEvidenceId = new Map(); ++ for (const item of items || []) { ++ const evidenceId = item && item.evidence && item.evidence.evidence_id; ++ if (typeof evidenceId !== "string" || typeof item.role !== "string") continue; ++ const roles = rolesByEvidenceId.get(evidenceId) || new Set(); ++ roles.add(item.role); ++ rolesByEvidenceId.set(evidenceId, roles); ++ if (roles.size > 1) { ++ return fail("STRATEGY_EVIDENCE_ALIAS", `evidence_id ${evidenceId} cannot satisfy distinct strategy roles`); ++ } ++ } ++ return { ok: true }; ++} ++ ++function assertRoleOrder(strategyName, items) { ++ const positions = new Map(); ++ for (let index = 0; index < (items || []).length; index += 1) { ++ const role = items[index] && items[index].role; ++ if (!positions.has(role)) positions.set(role, []); ++ positions.get(role).push(index); ++ } ++ const ordered = strategyName === "bug" ? ["red", "patch", "green"] : strategyName === "strict-tdd" ? ["red", "green"] : []; ++ for (let index = 0; index < ordered.length - 1; index += 1) { ++ const earlier = positions.get(ordered[index]) || []; ++ const later = positions.get(ordered[index + 1]) || []; ++ if (earlier.length > 0 && later.length > 0 && Math.max(...earlier) > Math.min(...later)) { ++ return fail("STRATEGY_SEQUENCE_VIOLATION", `${ordered[index]} evidence must precede ${ordered[index + 1]} evidence`); ++ } ++ } ++ return { ok: true }; ++} ++ + /** + * Check anyOf role groups and their provenance admission. + * Extracted so evaluateStrategy stays at most three control-flow levels. +@@ -148,6 +181,11 @@ function evaluateStrategy(strategyName, items) { + if (!policy) return fail("MISSING_STRATEGY_MINIMUM", `unknown strategy ${strategyName}`); + const roles = rolesOf(items); + ++ const distinctEvidence = assertDistinctRoleEvidence(items); ++ if (!distinctEvidence.ok) return distinctEvidence; ++ const roleOrder = assertRoleOrder(strategyName, items); ++ if (!roleOrder.ok) return roleOrder; ++ + if (strategyName === "feature" && policy.characterizationOnlyRoles) { + const hasChar = policy.characterizationOnlyRoles.some((role) => roles.has(role)); + const hasFeatureMin = roles.has("acceptance"); +@@ -210,4 +248,6 @@ module.exports = { + STRATEGY_TABLE, + selectStrategy, + evaluateStrategy, ++ assertDistinctRoleEvidence, ++ assertRoleOrder, + }; +diff --git a/scripts/lib/k2a-maturity-docs.test.js b/scripts/lib/k2a-maturity-docs.test.js +index 1586bdb..68426f9 100644 +--- a/scripts/lib/k2a-maturity-docs.test.js ++++ b/scripts/lib/k2a-maturity-docs.test.js +@@ -31,8 +31,8 @@ test("K2a maturity docs tag host surfaces implemented; Candidate/attestation/del + assert.match(roadmap, /K2a.*\*\*done\*\*|Headless Conformance Host.*implemented/i); + assert.match(roadmap, /\|\s*`done`\s*\|\s*\*\*K3\*\*/); + assert.match(roadmap, /\|\s*`done`\s*\|\s*\*\*K4b\*\*/); +- assert.match(roadmap, /\|\s*`done`\s*\|\s*\*\*K6b\*\*/); +- assert.match(roadmap, /\|\s*`next-eligible`\s*\|\s*\*\*K6c\*\*/); ++ assert.match(roadmap, /\|\s*`revise`\s*\|\s*\*\*K6b\*\*/); ++ assert.match(roadmap, /\|\s*`blocked-by-K6b-remediation`\s*\|\s*\*\*K6c\*\*/); + assert.match(arch, /\{implemented\} Independent verifier over frozen CandidateId \(K6b\)/); + assert.match(arch, /\{implemented\} Evidence strategies with provenance and Strict TDD fallback \(K6b\)/); + assert.match(arch, /\{implemented\} Assurance Graph as derived content-addressed projection/); +@@ -42,6 +42,6 @@ test("K2a maturity docs tag host surfaces implemented; Candidate/attestation/del + assert.doesNotMatch(arch, /\{implemented\}.*CandidateEvaluationAttestation/); + assert.doesNotMatch(roadmap, /\|\s*`in-progress`\s*\|\s*\*\*K4b\*\*/); + assert.doesNotMatch(roadmap, /\|\s*`in-progress`\s*\|\s*\*\*K6b\*\*/); +- assert.doesNotMatch(roadmap, /\|\s*`blocked`\s*\|\s*K6b\b/); ++ assert.doesNotMatch(roadmap, /\|\s*`blocked`\s*\|\s*\*\*K6b\*\*/); + assert.doesNotMatch(roadmap, /\|\s*`next-eligible`\s*\|\s*\*\*K6b\*\*/); + }); +diff --git a/scripts/lib/k6b-schema-fixtures.test.js b/scripts/lib/k6b-schema-fixtures.test.js +index 19ef9e3..2330d18 100644 +--- a/scripts/lib/k6b-schema-fixtures.test.js ++++ b/scripts/lib/k6b-schema-fixtures.test.js +@@ -105,6 +105,7 @@ test("K6b contract claims: additive families list required fields without replac + "node_id", + "candidate_id", + "policy_snapshot_id", ++ "evidence_requirements_satisfied", + ]); + }); + +@@ -248,6 +249,8 @@ test("K6b assessment/v1: valid fixture passes; verdict, missing fields, and cros + + const missing = readJson("schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json"); + assert.equal(validateInstance(assessmentSchema, missing).valid, false); ++ const missingCoverage = readJson("schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json"); ++ assert.equal(validateInstance(assessmentSchema, missingCoverage).valid, false); + + const withVerdict = readJson("schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json"); + const verdictRes = validateInstance(assessmentSchema, withVerdict); diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/decision.json b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/decision.json new file mode 100644 index 00000000..3803ee5c --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/decision.json @@ -0,0 +1,166 @@ +{ + "schema_version": 1, + "classification": "high-risk", + "evidence": { + "schema_version": 1, + "fingerprint": "sha256:35db40e519d337da13828b16e042956c911cae1d2f3c0239d1502d8b5c8a59ce", + "sources": { + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-semantic-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-semantic-integrity-remediation/design.md", + "openspec/changes/k6b-semantic-integrity-remediation/proposal.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/state.yaml", + "openspec/changes/k6b-semantic-integrity-remediation/tasks.md", + "openspec/changes/k6b-semantic-integrity-remediation/verify-report.md", + "openspec/memory/known-issues.md", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json", + "schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json", + "schemas/kernel/assessment/fixtures/valid/v1-complete.json", + "schemas/kernel/assessment/fixtures/valid/v1-four-roles.json", + "schemas/kernel/assessment/v1.schema.json", + "schemas/kernel/contract-claims.json", + "scripts/k6b-verifier-assurance-graph-e2e.test.js", + "scripts/lib/assurance-graph/index.js", + "scripts/lib/assurance-graph/index.test.js", + "scripts/lib/assurance-graph/projector.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/bindings.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.test.js", + "scripts/lib/independent-verifier/obligation-coverage.js", + "scripts/lib/independent-verifier/obligation-coverage.test.js", + "scripts/lib/independent-verifier/strategy-policy.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js" + ], + "capabilities": [ + "runtime" + ], + "operation_types": [ + "add", + "modify" + ], + "dependencies": [], + "facts": [ + { + "code": "design-risk", + "source": "design", + "detail": "Fail-closed verifier and Assurance Graph public APIs reject aliased roles, partial coverage, and tampered stored graphs." + }, + { + "code": "metadata-runtime", + "source": "metadata", + "detail": "runtime" + }, + { + "code": "diff-auth-permission", + "source": "real-diff", + "detail": "scripts/lib/independent-verifier/strategy-policy.js" + } + ] + } + }, + "generalist": { + "status": "needs-specialist", + "specialists": [ + "risk", + "reliability" + ], + "reason": "signals=design-risk,diff-auth-permission,metadata-runtime;dimensions=risk,reliability" + }, + "depth": { + "review": "strict" + }, + "escalation_reason": null, + "dimensions": { + "risk": { + "selected": true, + "reasons": [ + { + "code": "high-risk-override", + "source": "override", + "detail": "Classification requires full 4R", + "precedence": 0 + }, + { + "code": "diff-auth-permission", + "source": "real-diff", + "detail": "scripts/lib/independent-verifier/strategy-policy.js", + "precedence": 2 + }, + { + "code": "generalist-escalation", + "source": "generalist", + "detail": "signals=design-risk,diff-auth-permission,metadata-runtime;dimensions=risk,reliability", + "precedence": 3 + }, + { + "code": "design-risk", + "source": "design", + "detail": "Fail-closed verifier and Assurance Graph public APIs reject aliased roles, partial coverage, and tampered stored graphs.", + "precedence": 4 + } + ] + }, + "reliability": { + "selected": true, + "reasons": [ + { + "code": "high-risk-override", + "source": "override", + "detail": "Classification requires full 4R", + "precedence": 0 + }, + { + "code": "generalist-escalation", + "source": "generalist", + "detail": "signals=design-risk,diff-auth-permission,metadata-runtime;dimensions=risk,reliability", + "precedence": 3 + }, + { + "code": "metadata-runtime", + "source": "metadata", + "detail": "runtime", + "precedence": 5 + } + ] + }, + "resilience": { + "selected": true, + "reasons": [ + { + "code": "high-risk-override", + "source": "override", + "detail": "Classification requires full 4R", + "precedence": 0 + } + ] + }, + "readability": { + "selected": true, + "reasons": [ + { + "code": "high-risk-override", + "source": "override", + "detail": "Classification requires full 4R", + "precedence": 0 + } + ] + } + }, + "selected_specialists": [ + "risk", + "reliability", + "resilience", + "readability" + ] +} diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/evidence.json b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/evidence.json new file mode 100644 index 00000000..c749b840 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/evidence.json @@ -0,0 +1,69 @@ +{ + "schema_version": 1, + "classification": "high-risk", + "fingerprint": "sha256:35db40e519d337da13828b16e042956c911cae1d2f3c0239d1502d8b5c8a59ce", + "sources": { + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-semantic-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-semantic-integrity-remediation/design.md", + "openspec/changes/k6b-semantic-integrity-remediation/proposal.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/state.yaml", + "openspec/changes/k6b-semantic-integrity-remediation/tasks.md", + "openspec/changes/k6b-semantic-integrity-remediation/verify-report.md", + "openspec/memory/known-issues.md", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json", + "schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json", + "schemas/kernel/assessment/fixtures/valid/v1-complete.json", + "schemas/kernel/assessment/fixtures/valid/v1-four-roles.json", + "schemas/kernel/assessment/v1.schema.json", + "schemas/kernel/contract-claims.json", + "scripts/k6b-verifier-assurance-graph-e2e.test.js", + "scripts/lib/assurance-graph/index.js", + "scripts/lib/assurance-graph/index.test.js", + "scripts/lib/assurance-graph/projector.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/bindings.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.test.js", + "scripts/lib/independent-verifier/obligation-coverage.js", + "scripts/lib/independent-verifier/obligation-coverage.test.js", + "scripts/lib/independent-verifier/strategy-policy.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js" + ], + "capabilities": [ + "runtime" + ], + "operation_types": [ + "add", + "modify" + ], + "dependencies": [], + "facts": [ + { + "code": "design-risk", + "source": "design", + "detail": "Fail-closed verifier and Assurance Graph public APIs reject aliased roles, partial coverage, and tampered stored graphs." + }, + { + "code": "metadata-runtime", + "source": "metadata", + "detail": "runtime" + }, + { + "code": "diff-auth-permission", + "source": "real-diff", + "detail": "scripts/lib/independent-verifier/strategy-policy.js" + } + ] + } +} diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/gate-plan.json b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/gate-plan.json new file mode 100644 index 00000000..b3c0e4f9 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/gate-plan.json @@ -0,0 +1,194 @@ +{ + "planned": { + "status": "ready", + "run_generalist": true, + "dispatch": [ + "review-risk", + "review-reliability", + "review-resilience", + "review-readability" + ], + "archive_allowed": false, + "gate": { + "status": "ready", + "schema_version": 1, + "classification": "high-risk", + "evidence": { + "schema_version": 1, + "fingerprint": "sha256:35db40e519d337da13828b16e042956c911cae1d2f3c0239d1502d8b5c8a59ce", + "sources": { + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-semantic-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-semantic-integrity-remediation/design.md", + "openspec/changes/k6b-semantic-integrity-remediation/proposal.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/state.yaml", + "openspec/changes/k6b-semantic-integrity-remediation/tasks.md", + "openspec/changes/k6b-semantic-integrity-remediation/verify-report.md", + "openspec/memory/known-issues.md", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json", + "schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json", + "schemas/kernel/assessment/fixtures/valid/v1-complete.json", + "schemas/kernel/assessment/fixtures/valid/v1-four-roles.json", + "schemas/kernel/assessment/v1.schema.json", + "schemas/kernel/contract-claims.json", + "scripts/k6b-verifier-assurance-graph-e2e.test.js", + "scripts/lib/assurance-graph/index.js", + "scripts/lib/assurance-graph/index.test.js", + "scripts/lib/assurance-graph/projector.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/bindings.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.test.js", + "scripts/lib/independent-verifier/obligation-coverage.js", + "scripts/lib/independent-verifier/obligation-coverage.test.js", + "scripts/lib/independent-verifier/strategy-policy.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js" + ], + "capabilities": [ + "runtime" + ], + "operation_types": [ + "add", + "modify" + ], + "dependencies": [], + "facts": [ + { + "code": "design-risk", + "source": "design", + "detail": "Fail-closed verifier and Assurance Graph public APIs reject aliased roles, partial coverage, and tampered stored graphs." + }, + { + "code": "metadata-runtime", + "source": "metadata", + "detail": "runtime" + }, + { + "code": "diff-auth-permission", + "source": "real-diff", + "detail": "scripts/lib/independent-verifier/strategy-policy.js" + } + ] + } + }, + "generalist": { + "status": "needs-specialist", + "specialists": [ + "risk", + "reliability" + ], + "reason": "signals=design-risk,diff-auth-permission,metadata-runtime;dimensions=risk,reliability" + }, + "depth": { + "review": "strict" + }, + "escalation_reason": null, + "dimensions": { + "risk": { + "selected": true, + "reasons": [ + { + "code": "high-risk-override", + "source": "override", + "detail": "Classification requires full 4R", + "precedence": 0 + }, + { + "code": "diff-auth-permission", + "source": "real-diff", + "detail": "scripts/lib/independent-verifier/strategy-policy.js", + "precedence": 2 + }, + { + "code": "generalist-escalation", + "source": "generalist", + "detail": "signals=design-risk,diff-auth-permission,metadata-runtime;dimensions=risk,reliability", + "precedence": 3 + }, + { + "code": "design-risk", + "source": "design", + "detail": "Fail-closed verifier and Assurance Graph public APIs reject aliased roles, partial coverage, and tampered stored graphs.", + "precedence": 4 + } + ] + }, + "reliability": { + "selected": true, + "reasons": [ + { + "code": "high-risk-override", + "source": "override", + "detail": "Classification requires full 4R", + "precedence": 0 + }, + { + "code": "generalist-escalation", + "source": "generalist", + "detail": "signals=design-risk,diff-auth-permission,metadata-runtime;dimensions=risk,reliability", + "precedence": 3 + }, + { + "code": "metadata-runtime", + "source": "metadata", + "detail": "runtime", + "precedence": 5 + } + ] + }, + "resilience": { + "selected": true, + "reasons": [ + { + "code": "high-risk-override", + "source": "override", + "detail": "Classification requires full 4R", + "precedence": 0 + } + ] + }, + "readability": { + "selected": true, + "reasons": [ + { + "code": "high-risk-override", + "source": "override", + "detail": "Classification requires full 4R", + "precedence": 0 + } + ] + } + } + } + }, + "plannedLineage": { + "status": "reviewing", + "next_action": { + "type": "await-lenses", + "dimensions": [ + "risk", + "reliability", + "resilience", + "readability" + ] + }, + "dispatch": [], + "archive_allowed": false + }, + "requestIds": { + "risk": "k6b-sem-4r-risk-1", + "reliability": "k6b-sem-4r-reliability-1", + "resilience": "k6b-sem-4r-resilience-1", + "readability": "k6b-sem-4r-readability-1" + } +} diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/lineage.json b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/lineage.json new file mode 100644 index 00000000..b5be7a7d --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/lineage.json @@ -0,0 +1,271 @@ +{ + "schema_version": 1, + "lineage_id": "sha256:8c6808007fb67e490cfd80cf38a8d40cef42bcf0f0e6b1a87bd73c7ee661906d", + "generation": 1, + "predecessor_lineage_id": null, + "recovery": null, + "revision": 9, + "status": "approved", + "genesis": { + "candidate": { + "projection": "workspace", + "base_tree": "f3d4073d5dadf42db7bc8ab74af72961c3fb3ca0", + "candidate_tree": "sha256:2f34dec4f2c0c1ef87d244946c87f4e3e93497ac9b82e56d20bad41c1b26184d", + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-semantic-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-semantic-integrity-remediation/design.md", + "openspec/changes/k6b-semantic-integrity-remediation/proposal.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/state.yaml", + "openspec/changes/k6b-semantic-integrity-remediation/tasks.md", + "openspec/changes/k6b-semantic-integrity-remediation/verify-report.md", + "openspec/memory/known-issues.md", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json", + "schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json", + "schemas/kernel/assessment/fixtures/valid/v1-complete.json", + "schemas/kernel/assessment/fixtures/valid/v1-four-roles.json", + "schemas/kernel/assessment/v1.schema.json", + "schemas/kernel/contract-claims.json", + "scripts/k6b-verifier-assurance-graph-e2e.test.js", + "scripts/lib/assurance-graph/index.js", + "scripts/lib/assurance-graph/index.test.js", + "scripts/lib/assurance-graph/projector.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/bindings.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.test.js", + "scripts/lib/independent-verifier/obligation-coverage.js", + "scripts/lib/independent-verifier/obligation-coverage.test.js", + "scripts/lib/independent-verifier/strategy-policy.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js" + ], + "diff_hash": "sha256:2f34dec4f2c0c1ef87d244946c87f4e3e93497ac9b82e56d20bad41c1b26184d", + "paths_digest": "sha256:cc512c9dc6f4ccdb570b60ab2d89d885c6a807a68c085dacec84520c3bcc842b", + "authored_lines": 1589, + "original_changed_lines": 1683 + }, + "candidate_id": "sha256:68cb610148f9b58096cda3b1c21bc042b65cbd70f78463d49d82afa42aa053c3", + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-semantic-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-semantic-integrity-remediation/design.md", + "openspec/changes/k6b-semantic-integrity-remediation/proposal.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/state.yaml", + "openspec/changes/k6b-semantic-integrity-remediation/tasks.md", + "openspec/changes/k6b-semantic-integrity-remediation/verify-report.md", + "openspec/memory/known-issues.md", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json", + "schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json", + "schemas/kernel/assessment/fixtures/valid/v1-complete.json", + "schemas/kernel/assessment/fixtures/valid/v1-four-roles.json", + "schemas/kernel/assessment/v1.schema.json", + "schemas/kernel/contract-claims.json", + "scripts/k6b-verifier-assurance-graph-e2e.test.js", + "scripts/lib/assurance-graph/index.js", + "scripts/lib/assurance-graph/index.test.js", + "scripts/lib/assurance-graph/projector.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/bindings.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.test.js", + "scripts/lib/independent-verifier/obligation-coverage.js", + "scripts/lib/independent-verifier/obligation-coverage.test.js", + "scripts/lib/independent-verifier/strategy-policy.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js" + ], + "classification": "high-risk", + "selected_dimensions": [ + "risk", + "reliability", + "resilience", + "readability" + ], + "evidence_fingerprint": "sha256:35db40e519d337da13828b16e042956c911cae1d2f3c0239d1502d8b5c8a59ce", + "original_changed_lines": 1683, + "authored_lines": 1589 + }, + "current_candidate_id": "sha256:68cb610148f9b58096cda3b1c21bc042b65cbd70f78463d49d82afa42aa053c3", + "current_candidate": { + "projection": "workspace", + "base_tree": "f3d4073d5dadf42db7bc8ab74af72961c3fb3ca0", + "candidate_tree": "sha256:2f34dec4f2c0c1ef87d244946c87f4e3e93497ac9b82e56d20bad41c1b26184d", + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-semantic-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-semantic-integrity-remediation/design.md", + "openspec/changes/k6b-semantic-integrity-remediation/proposal.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/state.yaml", + "openspec/changes/k6b-semantic-integrity-remediation/tasks.md", + "openspec/changes/k6b-semantic-integrity-remediation/verify-report.md", + "openspec/memory/known-issues.md", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json", + "schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json", + "schemas/kernel/assessment/fixtures/valid/v1-complete.json", + "schemas/kernel/assessment/fixtures/valid/v1-four-roles.json", + "schemas/kernel/assessment/v1.schema.json", + "schemas/kernel/contract-claims.json", + "scripts/k6b-verifier-assurance-graph-e2e.test.js", + "scripts/lib/assurance-graph/index.js", + "scripts/lib/assurance-graph/index.test.js", + "scripts/lib/assurance-graph/projector.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/bindings.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.test.js", + "scripts/lib/independent-verifier/obligation-coverage.js", + "scripts/lib/independent-verifier/obligation-coverage.test.js", + "scripts/lib/independent-verifier/strategy-policy.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js" + ], + "diff_hash": "sha256:2f34dec4f2c0c1ef87d244946c87f4e3e93497ac9b82e56d20bad41c1b26184d", + "paths_digest": "sha256:cc512c9dc6f4ccdb570b60ab2d89d885c6a807a68c085dacec84520c3bcc842b", + "authored_lines": 1589, + "original_changed_lines": 1683 + }, + "lenses": { + "risk": { + "selected": true, + "status": "completed", + "request_id": "k6b-sem-4r-risk-1", + "result_digest": "sha256:0fa38eeaca8882e0d194fd5ba5d8378917d98e795204f8f7a9dfdb78f111f3cc", + "result": { + "findings": [] + }, + "operation": null, + "result_request_id": "k6b-sem-4r-risk-1" + }, + "reliability": { + "selected": true, + "status": "completed", + "request_id": "k6b-sem-4r-reliability-1", + "result_digest": "sha256:70bb77e2ed7b1d6a02e24d582f70d9994410a137f9ddd976386d34e6e7ca6e05", + "result": { + "findings": [ + { + "severity": "WARNING", + "summary": "El preflight de replay revalida schema, identidad y bindings de cada assessment, pero no hay test del fallo token-level MUST (assessments vacíos o unión incompleta). Ese camino es código nuevo duplicado respecto de walkMustObligations y puede regresionar en silencio.", + "acceptance_criteria": "Añadir un test que construya persistable válido, deje assessments=[] o un assessment que solo satisface token A de required_evidence [A,B] en un MUST no diferido, llame replayAssuranceGraph y afirme ok===false, reason_code==='GRAPH_DIVERGENCE', y que el error identifique la obligación. El happy-path de replay byte-identical debe seguir pasando." + }, + { + "severity": "WARNING", + "summary": "reconcileAssuranceGraph compara nodes/edges contra la proyección, pero ningún test llega a esa rama con un graph_id autoconsistente. El escenario de nodos adulterados queda cubierto solo por mismatch de digest, que ya existía antes de comparar el payload completo.", + "acceptance_criteria": "Añadir un caso withStoredGraphId que altere nodes (nodo extra o propiedad persistible de más) y, aparte, uno equivalente para edges si aplica; reconcileAssuranceGraph debe devolver GRAPH_DIVERGENCE. Quitar «nodes» de fields debe hacer fallar ese test. No basta con añadir un nodo sin recomputar graph_id." + } + ] + }, + "operation": null, + "result_request_id": "k6b-sem-4r-reliability-1" + }, + "resilience": { + "selected": true, + "status": "completed", + "request_id": "k6b-sem-4r-resilience-1", + "result_digest": "sha256:0fa38eeaca8882e0d194fd5ba5d8378917d98e795204f8f7a9dfdb78f111f3cc", + "result": { + "findings": [] + }, + "operation": null, + "result_request_id": "k6b-sem-4r-resilience-1" + }, + "readability": { + "selected": true, + "status": "completed", + "request_id": "k6b-sem-4r-readability-1", + "result_digest": "sha256:70f57b10ce58fb1038f06cef8b5b2b3118d906fcc1951c8e473dabf95127b0b7", + "result": { + "findings": [ + { + "severity": "WARNING", + "summary": "assertRoleOrder elige la secuencia con un ternario anidado y falla con max(predecesor) > min(sucesor) sin documentar que todo el rol previo debe terminar antes del primero del siguiente.", + "acceptance_criteria": "Extraer las secuencias a un campo nombrado (p. ej. en STRATEGY_TABLE). Comentar que el último índice del rol i debe ser menor que el primero del rol i+1, y que [] omite la comprobación en el resto de estrategias. Sin ternario anidado." + }, + { + "severity": "WARNING", + "summary": "normalizedCoverage lee cobertura del wrapper o de item.evidence con || y sin comentario sobre cuál forma es canónica.", + "acceptance_criteria": "Leer un solo campo (el wrapper clasificado) o documentar en un comentario por qué existe el fallback anidado. No aceptar ambas formas en silencio." + } + ] + }, + "operation": null, + "result_request_id": "k6b-sem-4r-readability-1" + } + }, + "findings": [ + { + "id": "F-7b201cb9066cda9c", + "owner": "reliability", + "severity": "WARNING", + "summary": "El preflight de replay revalida schema, identidad y bindings de cada assessment, pero no hay test del fallo token-level MUST (assessments vacíos o unión incompleta). Ese camino es código nuevo duplicado respecto de walkMustObligations y puede regresionar en silencio.", + "acceptance_criteria": "Añadir un test que construya persistable válido, deje assessments=[] o un assessment que solo satisface token A de required_evidence [A,B] en un MUST no diferido, llame replayAssuranceGraph y afirme ok===false, reason_code==='GRAPH_DIVERGENCE', y que el error identifique la obligación. El happy-path de replay byte-identical debe seguir pasando.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-6517eb45a348699b", + "owner": "reliability", + "severity": "WARNING", + "summary": "reconcileAssuranceGraph compara nodes/edges contra la proyección, pero ningún test llega a esa rama con un graph_id autoconsistente. El escenario de nodos adulterados queda cubierto solo por mismatch de digest, que ya existía antes de comparar el payload completo.", + "acceptance_criteria": "Añadir un caso withStoredGraphId que altere nodes (nodo extra o propiedad persistible de más) y, aparte, uno equivalente para edges si aplica; reconcileAssuranceGraph debe devolver GRAPH_DIVERGENCE. Quitar «nodes» de fields debe hacer fallar ese test. No basta con añadir un nodo sin recomputar graph_id.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-67351c60ec031360", + "owner": "readability", + "severity": "WARNING", + "summary": "assertRoleOrder elige la secuencia con un ternario anidado y falla con max(predecesor) > min(sucesor) sin documentar que todo el rol previo debe terminar antes del primero del siguiente.", + "acceptance_criteria": "Extraer las secuencias a un campo nombrado (p. ej. en STRATEGY_TABLE). Comentar que el último índice del rol i debe ser menor que el primero del rol i+1, y que [] omite la comprobación en el resto de estrategias. Sin ternario anidado.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-089aebf42d0e8d12", + "owner": "readability", + "severity": "WARNING", + "summary": "normalizedCoverage lee cobertura del wrapper o de item.evidence con || y sin comentario sobre cuál forma es canónica.", + "acceptance_criteria": "Leer un solo campo (el wrapper clasificado) o documentar en un comentario por qué existe el fallback anidado. No aceptar ambas formas en silencio.", + "blocking": false, + "resolution": "advisory" + } + ], + "findings_digest": "sha256:c3a1b69119bef49f38c9d8fc9a00af00f3a2d3b09aeb57051c34d325ec09c4aa", + "correction_budget": { + "limit_lines": 200, + "used_lines": 0, + "failed_attempts": 0, + "max_failed_attempts": 3 + }, + "correction_history": [], + "validation_history": [], + "follow_ups": [], + "pending_operation": null, + "pending_correction": null, + "terminal_reason": "no-unresolved-blocking-findings" +} diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/summary.json b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/summary.json new file mode 100644 index 00000000..c6c67e87 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/.4r/summary.json @@ -0,0 +1,73 @@ +{ + "path_count": 35, + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-semantic-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-semantic-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-semantic-integrity-remediation/design.md", + "openspec/changes/k6b-semantic-integrity-remediation/proposal.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-semantic-integrity-remediation/state.yaml", + "openspec/changes/k6b-semantic-integrity-remediation/tasks.md", + "openspec/changes/k6b-semantic-integrity-remediation/verify-report.md", + "openspec/memory/known-issues.md", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json", + "schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json", + "schemas/kernel/assessment/fixtures/valid/v1-complete.json", + "schemas/kernel/assessment/fixtures/valid/v1-four-roles.json", + "schemas/kernel/assessment/v1.schema.json", + "schemas/kernel/contract-claims.json", + "scripts/k6b-verifier-assurance-graph-e2e.test.js", + "scripts/lib/assurance-graph/index.js", + "scripts/lib/assurance-graph/index.test.js", + "scripts/lib/assurance-graph/projector.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/bindings.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.test.js", + "scripts/lib/independent-verifier/obligation-coverage.js", + "scripts/lib/independent-verifier/obligation-coverage.test.js", + "scripts/lib/independent-verifier/strategy-policy.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js" + ], + "operation_types": [ + "add", + "modify" + ], + "facts": [ + { + "code": "design-risk", + "source": "design", + "detail": "Fail-closed verifier and Assurance Graph public APIs reject aliased roles, partial coverage, and tampered stored graphs." + }, + { + "code": "metadata-runtime", + "source": "metadata", + "detail": "runtime" + }, + { + "code": "diff-auth-permission", + "source": "real-diff", + "detail": "scripts/lib/independent-verifier/strategy-policy.js" + } + ], + "fingerprint": "sha256:35db40e519d337da13828b16e042956c911cae1d2f3c0239d1502d8b5c8a59ce", + "counts": { + "added": 1589, + "removed": 94, + "original_changed_lines": 1683, + "authored_lines": 1589 + }, + "diff_sha256": "sha256:2f34dec4f2c0c1ef87d244946c87f4e3e93497ac9b82e56d20bad41c1b26184d", + "paths_digest": "sha256:cc512c9dc6f4ccdb570b60ab2d89d885c6a807a68c085dacec84520c3bcc842b", + "head": "f3d4073d5dadf42db7bc8ab74af72961c3fb3ca0", + "diff_path": "openspec/changes/k6b-semantic-integrity-remediation/.4r/candidate.diff", + "evidence_path": "openspec/changes/k6b-semantic-integrity-remediation/.4r/evidence.json" +} diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/apply-progress.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/apply-progress.md new file mode 100644 index 00000000..70ca6c0e --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/apply-progress.md @@ -0,0 +1,43 @@ +# Apply Progress: k6b Semantic Integrity Remediation + +## Batch 1 — 2026-08-27 + +Delivery path: `size:exception` accepted by the maintainer. Scope remained focal to B1–B3/H1–H3; 22/22 checklist tasks are implemented and locally verified. (The task artifact contains 22 items despite the prior forecast referring to 21.) + +| Task range | Status | Local verification | Result | +| --- | --- | --- | --- | +| 1.1–1.5 | [x] | `node --test ...k6b-schema-fixtures.test.js ...assessment.test.js` | Required canonical coverage is schema-valid, included in assessment identity, and frozen K1/v2 bytes remain pinned. | +| 2.1–2.7 | [x] | `node --test scripts/lib/independent-verifier/*.test.js` | Contract gate runs before strategy; incompatible evidence roles, invalid order, unknown/wrong bindings, and partial token coverage fail closed. | +| 3.1–3.6 | [x] | `node --test scripts/lib/assurance-graph/index.test.js` | Project, replay, and reconcile validate canonical digests and complete stored graph payloads fail-closed. | +| 4.1–4.4 | [x] | Focused suite: 79/79 tests passed; full `npm test` completed with `All checks passed.` | E2E is deterministic; K6b is `revise` and K6c blocked until archive; no legacy assessment migration was introduced. | + +## Files changed + +- Assessment contract and fixtures: `schemas/kernel/assessment/v1.schema.json`, `schemas/kernel/contract-claims.json`, and assessment fixtures. +- Verifier: canonical assessment validation, contract binding, evidence coverage normalization, ordered non-aliased strategy checks, and token-subset MUST coverage. +- Assurance Graph: resolved canonical-input validation, replay assessment revalidation, stored-payload graph-id recomputation, and complete reconciliation comparison. +- Tests and E2E coverage under `scripts/lib/**` and `scripts/k6b-verifier-assurance-graph-e2e.test.js`. +- Roadmap and architecture status for the active remediation. + +## Migration and rollback + +No migration is attempted. Legacy `assessment/v1` records without `evidence_requirements_satisfied` fail closed and must be regenerated from canonical verifier inputs. Roll back schema, verifier, graph hardening, fixtures, tests, and temporary roadmap state together; `evidence/v2`, `verification/v2`, and K1 v1 remain unmodified. + +## Deviations and risks + +None from the approved design. The repository had a pre-existing `models.yaml` modification; it was preserved and is outside this change scope. The implementation delta is approximately 450 changed lines, below the 850–1,150 forecast, so no workload escalation occurred. + +## Batch 2 — 2026-08-27 + +Delivery path remains the maintainer-approved `size:exception`. This focused remediation completes 4/4 verify-gap tasks (26/26 total) with persistent runtime tests only; no production source, schema, fixture, or contract file changed. + +| Task range | Status | Local verification | Result | +| --- | --- | --- | --- | +| 2.8 | [x] | `node --test scripts/lib/independent-verifier/assessment.test.js scripts/lib/assurance-graph/index.test.js` (21/21) | Assessment identity changes independently for `evidence_id` and `obligation_id`. | +| 3.7 | [x] | Focused verifier/Assurance Graph suite (72/72) | Replay rejects malformed schema, invalid coverage, candidate/policy mismatch, missing evidence, unknown obligation, non-implementing node, and `node_id` mismatch with `GRAPH_DIVERGENCE`. | +| 3.8 | [x] | Focused verifier/Assurance Graph suite (72/72) | Reconcile rejects tampered stored canonical inputs, candidate, kind, and schema after recomputing the stored graph id. | +| 5.1 | [x] | `npm test` (exit 0); `git diff --check` | Full suite passed; no scoped implementation mismatch was exposed. | + +## Batch 2 deviations and risks + +None. The new tests exercised the existing fail-closed implementation without requiring a production-code correction. Pre-existing changes, including `models.yaml`, remain preserved and outside this batch. diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/archive-plan.json b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/archive-plan.json new file mode 100644 index 00000000..3380cf11 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/archive-plan.json @@ -0,0 +1,78 @@ +{ + "schema_version": 1, + "change": "k6b-semantic-integrity-remediation", + "source_fingerprint": "sha256:a3a3f75b46420918f61adb3fda750d8775fd41e414a460eea19508daffcd6d8d", + "spec_writes": [ + { + "domain": "independent-verification", + "source_delta": "prepared-specs/independent-verification/spec.md", + "target": "openspec/specs/independent-verification/spec.md", + "target_before_sha256": "sha256:3207ce8b5b280472b9b505378cf91df22b1ae22e007adb9dbf8a173405c5b0a0", + "content_sha256": "sha256:4458c5773ceefc0453b0ca5ff73da7bc916aab239e7f5203c748e4a9db519da2" + }, + { + "domain": "assurance-graph", + "source_delta": "prepared-specs/assurance-graph/spec.md", + "target": "openspec/specs/assurance-graph/spec.md", + "target_before_sha256": "sha256:81f10f2046ae5519aead34f0f945275e99bfefbc03f9a898d80ce8bcbc437f56", + "content_sha256": "sha256:c06f59378d4acb1bfefa17bf02e4df98e423a6a26540d75fdb6bd820b6aed73d" + }, + { + "domain": "kernel-contract-schemas", + "source_delta": "prepared-specs/kernel-contract-schemas/spec.md", + "target": "openspec/specs/kernel-contract-schemas/spec.md", + "target_before_sha256": "sha256:0f6c1f85aaff2f9b7a60320a66830bbc23feeb01a29ce4e6a8ef8f5d59ef4cec", + "content_sha256": "sha256:0ef263964a2212696ea724c4c3bc51774e8b5d739cf9dbd49b43cdf7e7f2b6d1" + } + ], + "adr_promotions": [ + { + "source": "decisions/adr-001.md", + "target": "docs/adr/adr-20260828-001-persist-token-coverage-on-assessment-v1.md", + "content_sha256": "sha256:e6ab50ceb55fa620900d3eaa225fb851ea1cefc4a961d9b54c883d934c89a066" + }, + { + "source": "decisions/adr-002.md", + "target": "docs/adr/adr-20260828-002-ordered-non-aliased-strategy-evidence.md", + "content_sha256": "sha256:70f342f1a10da935769673d1da846b5410730150f2946db49d956eb0543e058c" + }, + { + "source": "decisions/adr-003.md", + "target": "docs/adr/adr-20260828-003-canonical-integrity-validation-across-graph-operations.md", + "content_sha256": "sha256:719bf46846df086500e73ea4cca8ad0fe16ef145a12bd4a5a0a3c117afb2ff20" + } + ], + "archive_inventory": [ + ".4r/candidate.diff", + ".4r/decision.json", + ".4r/evidence.json", + ".4r/gate-plan.json", + ".4r/lineage.json", + ".4r/summary.json", + "apply-progress.md", + "archive-report.md", + "decisions/adr-001.md", + "decisions/adr-002.md", + "decisions/adr-003.md", + "design.md", + "prepared-specs/assurance-graph/spec.md", + "prepared-specs/independent-verification/spec.md", + "prepared-specs/kernel-contract-schemas/spec.md", + "proposal.md", + "specs/assurance-graph/spec.md", + "specs/independent-verification/spec.md", + "specs/kernel-contract-schemas/spec.md", + "state.yaml", + "tasks.md", + "verify-report.md" + ], + "accepted_warnings": [ + "4r-warning-001 WARNING: Replay MUST-incomplete path (empty/partial assessments) lacks a dedicated runtime test — scripts/lib/assurance-graph/index.js / tests", + "4r-warning-002 WARNING: Reconcile nodes/edges comparison is not exercised with a self-consistent stored graph_id", + "4r-warning-003 WARNING: assertRoleOrder nested ternary / undocumented last-predecessor-before-first-successor criterion", + "4r-warning-004 WARNING: normalizedCoverage silent dual-shape fallback (wrapper || nested evidence)" + ], + "rollback": { + "strategy": "staging-rename" + } +} diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/archive-report.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/archive-report.md new file mode 100644 index 00000000..a024e887 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/archive-report.md @@ -0,0 +1,89 @@ +# Archive Report: k6b-semantic-integrity-remediation + +**Archive destination (planned)**: `openspec/changes/archive/2026-08-28-k6b-semantic-integrity-remediation/` +**Verified**: 2026-08-27 +**Verify verdict**: PASS (26/26 tasks; 22/22 MUST scenarios at runtime-test; focused 82/82; `npm test` 2762 pass / 0 fail) + +## Summary + +Post-v2.51.0 semantic integrity remediation closes six defects (B1–B3, H1–H3): invert REQ-006 aliasing so incompatible strategy roles MUST NOT share one EvidenceId; enforce token-subset MUST coverage persisted on `assessment/v1`; gate contract digest before strategy; harden `projectAssuranceGraph`, `replayAssuranceGraph`, and `reconcileAssuranceGraph` fail-closed. Frozen `evidence/v2`, `verification/v2`, and K1 v1 bytes remain unchanged. Implementation spans verifier, Assurance Graph, kernel assessment schema/fixtures, adversarial tests, and harness-evolution docs. All 26 tasks complete under approved `size:exception` delivery. Candidate excludes pre-existing `models.yaml`. + +## Verification Gate + +| Check | Result | +|-------|--------| +| Verify verdict | PASS | +| CRITICAL issues | None | +| WARNING issues (verify) | None | +| Apply tasks complete | 26/26 | +| 4R review gate | approved (`archive_allowed: true`; lineage `sha256:8c6808007fb67e490cfd80cf38a8d40cef42bcf0f0e6b1a87bd73c7ee661906d`) | +| 4R findings | 0 BLOCKER, 0 CRITICAL, 4 WARNING, 0 SUGGESTION | +| Baseline fingerprints | Match `state.yaml` for all three delta domains | + +## Spec Preparation (change-local) + +| Domain | Action | Added | Modified | Removed | +|--------|--------|-------|----------|---------| +| `independent-verification` | Prepared merge | REQ-008 (1) | REQ-005, REQ-006 (2) | — | +| `assurance-graph` | Prepared merge | REQ-007, REQ-008 (2) | REQ-006 (1) | — | +| `kernel-contract-schemas` | Prepared merge | — | REQ-027 (1) | — | + +Prepared bytes: + +- `prepared-specs/independent-verification/spec.md` (`sha256:4458c5773ceefc0453b0ca5ff73da7bc916aab239e7f5203c748e4a9db519da2`) +- `prepared-specs/assurance-graph/spec.md` (`sha256:c06f59378d4acb1bfefa17bf02e4df98e423a6a26540d75fdb6bd820b6aed73d`) +- `prepared-specs/kernel-contract-schemas/spec.md` (`sha256:0ef263964a2212696ea724c4c3bc51774e8b5d739cf9dbd49b43cdf7e7f2b6d1`) + +Live `target_before_sha256` values (from `state.yaml` `baseline_fingerprints`): + +- `independent-verification`: `sha256:3207ce8b5b280472b9b505378cf91df22b1ae22e007adb9dbf8a173405c5b0a0` +- `assurance-graph`: `sha256:81f10f2046ae5519aead34f0f945275e99bfefbc03f9a898d80ce8bcbc437f56` +- `kernel-contract-schemas`: `sha256:0f6c1f85aaff2f9b7a60320a66830bbc23feeb01a29ce4e6a8ef4cec` + +Live `openspec/specs/**` writes are runtime-owned. + +## ADR Promotions (planned) + +| Source | Planned target | +|--------|----------------| +| `decisions/adr-001.md` | `docs/adr/adr-20260828-001-persist-token-coverage-on-assessment-v1.md` | +| `decisions/adr-002.md` | `docs/adr/adr-20260828-002-ordered-non-aliased-strategy-evidence.md` | +| `decisions/adr-003.md` | `docs/adr/adr-20260828-003-canonical-integrity-validation-across-graph-operations.md` | + +Change-local copies under `decisions/` travel with the archive folder as audit trail. + +## Accepted Risks / Follow-ups + +| ID | Severity | Owner | Summary | Disposition | +|----|----------|-------|---------|-------------| +| 4r-warning-001 | WARNING | assurance-graph | Replay MUST-incomplete path (empty/partial assessments) lacks a dedicated runtime test | Non-blocking follow-up | +| 4r-warning-002 | WARNING | assurance-graph | Reconcile `nodes`/`edges` comparison is not exercised with a self-consistent stored `graph_id` | Non-blocking follow-up | +| 4r-warning-003 | WARNING | independent-verifier | `assertRoleOrder` nested ternary / undocumented last-predecessor-before-first-successor criterion | Non-blocking follow-up | +| 4r-warning-004 | WARNING | independent-verifier | `normalizedCoverage` silent dual-shape fallback (`wrapper \|\| nested evidence`) | Non-blocking follow-up | + +4R advisory WARNINGs are non-blocking; covering runtime tests pass for the archived scope. + +## Archive Inventory + +Origin paths preserved by the planned runtime move (excluding `archive-plan.json` from fingerprint identity): proposal, design, tasks, apply/verify/archive reports, delta and prepared specs, three decisions, state, `.4r/` review lineage artifacts (6 files), and remediation scope (22 entries at plan emission). + +## Runtime Completion (pending) + +- Live spec merge and ADR promotion: `node scripts/archive-transaction-run.js k6b-semantic-integrity-remediation` +- Source directory `openspec/changes/k6b-semantic-integrity-remediation/` still exists until runtime receipt confirms full match and delete-after-commit. + +## Cost + +Estimated token cost per phase, aggregated from +`.ospec/session/k6b-semantic-integrity-remediation/phase-costs.jsonl`. Figures are heuristic estimates +(~4 bytes/token), not exact metering. + +| Phase | Invocations | Re-launches | Duration | Model Tiers | Statuses | Estimated Prompt Tokens | Estimated Artifact Tokens | Estimated Tool Output Tokens | Estimated Output Tokens | +|-------|-------------|-------------|----------|-------------|----------|-------------------------|---------------------------|------------------------------|-------------------------| +| spec | 1 | 0 | 0ms | unknown | blocked | 46308 (estimated) | 0 (estimated) | 0 (estimated) | 678 (estimated) | +| design | 1 | 0 | 0ms | unknown | success | 56219 (estimated) | 0 (estimated) | 0 (estimated) | 29 (estimated) | +| tasks | 2 | 1 | 0ms | unknown | success | 159888 (estimated) | 0 (estimated) | 0 (estimated) | 1605 (estimated) | +| apply | 2 | 1 | 0ms | unknown | success | 181179 (estimated) | 0 (estimated) | 0 (estimated) | 939 (estimated) | +| verify | 1 | 0 | 0ms | unknown | success | 88482 (estimated) | 0 (estimated) | 0 (estimated) | 29 (estimated) | + +**Total user questions asked**: 0 diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/decisions/adr-001.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/decisions/adr-001.md new file mode 100644 index 00000000..056e9a6e --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/decisions/adr-001.md @@ -0,0 +1,23 @@ +# ADR-001: Persist token coverage on assessment/v1 + +- Status: proposed +- Change: k6b-semantic-integrity-remediation +- Date: 2026-08-27 + +## Context + +Existential evidence binding cannot prove `required_evidence` subset coverage. Coverage belongs to an evaluated evidence/role/obligation tuple, while `evidence/v2` and K1 contracts are frozen. + +## Decision + +Add required `evidence_requirements_satisfied` to `assessment/v1` as a unique, canonical string array. Include it in the `assessment_id` preimage and in assessment required claims. Omission fails schema validation. + +## Alternatives + +- Put tokens on `evidence/v2`: rejected; it mutates observation identity. +- Keep coverage ephemeral: rejected; replay cannot validate it. +- Publish `assessment/v2`: rejected; the accepted delta evolves the additive v1 family and requires no parallel family. + +## Consequences + +Old partial assessments fail closed and must be regenerated. Coverage tampering changes identity. Evidence, verification, and K1 schema bytes stay frozen; rollback is a unit revert. diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/decisions/adr-002.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/decisions/adr-002.md new file mode 100644 index 00000000..81d534c7 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/decisions/adr-002.md @@ -0,0 +1,23 @@ +# ADR-002: Ordered, non-aliased strategy evidence + +- Status: proposed +- Change: k6b-semantic-integrity-remediation +- Date: 2026-08-27 + +## Context + +A Set of roles proves neither distinct observations nor temporal order. Adding timestamps to frozen `evidence/v2` would create a new clock contract. + +## Decision + +Evaluate normalized evidence in `rawEvidence` list order. Within the selected strategy, one EvidenceId may bind repeatedly to one role but not to distinct semantic roles. Require RED before GREEN for Strict TDD and RED before PATCH before GREEN for bug strategy. + +## Alternatives + +- Keep role-set evaluation: rejected; aliasing and reversed sequences pass. +- Add persisted timestamps: rejected; mutates the frozen evidence family and introduces clock authority. +- Infer order from digests: rejected; content hashes carry no chronology. + +## Consequences + +Callers must provide evidence in observed sequence and use distinct observations for distinct roles. Reordering becomes semantically observable but remains cheap to correct. Schema-only four-role fixtures remain valid; combined verifier use fails closed. diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/decisions/adr-003.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/decisions/adr-003.md new file mode 100644 index 00000000..7b0935af --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/decisions/adr-003.md @@ -0,0 +1,23 @@ +# ADR-003: Canonical integrity validation across graph operations + +- Status: proposed +- Change: k6b-semantic-integrity-remediation +- Date: 2026-08-27 + +## Context + +The projector coalesces contradictory canonical inputs, replay trusts assessments, and reconcile compares only graph id plus edges. Direct callers can therefore bypass facade checks or hide stored-payload tampering. + +## Decision + +Share deterministic canonical-input and graph-id helpers. Project rejects contradictory or unresolved digests before hashing; replay schema-validates and recomputes every assessment plus graph binding; reconcile recomputes the stored payload identity and compares the complete canonical graph. + +## Alternatives + +- Harden only `verifyCandidate`: rejected; direct graph APIs remain permissive. +- Compare graph id and edges only: rejected; nodes, subject, canonical inputs, and kind/schema remain unchecked. +- Trust a recomputed assessment id alone: rejected; graph/evidence/obligation bindings still need validation. + +## Consequences + +Project, replay, and reconcile share one fail-closed identity model. Legacy incomplete payloads fail instead of being repaired silently. The change adds validation work but no dependency or migration. diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/design.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/design.md new file mode 100644 index 00000000..aca00943 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/design.md @@ -0,0 +1,147 @@ +# Design: k6b-semantic-integrity-remediation + +## Technical Approach + +Harden the existing CommonJS verifier and Assurance Graph pipeline in place. The change adds three ordered gates before a successful verdict: canonical contract binding, strategy semantics (non-aliased roles and temporal order), and token-level MUST coverage. Persisted `assessment/v1` records gain the required `evidence_requirements_satisfied` field; the sorted unique value participates in `assessment_id`. `evidence/v2`, `verification/v2`, and all K1 v1 bytes/pins remain unchanged. + +The Assurance Graph projector will validate resolved canonical inputs before hashing. Replay will schema-validate and recompute every assessment before projection. Reconcile will validate the entire stored payload and independently recompute its declared `graph_id`, rather than trusting `graph_id` plus edges. + +This confirms `sdd-propose-001` and `sdd-spec-001`: coverage stays on `assessment/v1`, the exact field is `evidence_requirements_satisfied`, and it is schema-required. + +## Architecture Decisions + +### Decision: Required canonical coverage on `assessment/v1` (ADR-001) + +**Choice**: Add required `evidence_requirements_satisfied` to `assessment/v1`; canonicalize it and include it in `assessment_id`. + +**Alternatives considered**: Mutating frozen `evidence/v2`; keeping coverage ephemeral; publishing a parallel assessment version. + +**Rationale**: Coverage belongs to an evidence/role/obligation binding. Persisting it makes partial coverage and tampering replay-verifiable, at the cost of regenerating old partial assessments. + +### Decision: Strategy role identity and order are evaluated before MUST coverage (ADR-002) + +**Choice**: Use normalized EvidenceIds and `rawEvidence` order. Distinct roles cannot share one id; `bug` requires RED* < PATCH* < GREEN* and `strict-tdd` RED* < GREEN*. + +**Alternatives considered**: A role Set, which loses identity/order; timestamps, which mutate frozen evidence and create clock authority; digest ordering, which has no chronology. + +**Rationale**: The existing input is ordered and repeated evidence for one role can remain valid. Strategy violations fail before the MUST walk. + +### Decision: One canonical integrity path for project, replay, and reconcile (ADR-003) + +**Choice**: Share canonical-input and graph-id helpers across project, replay, and reconcile. + +**Alternatives considered**: Hardening only `verifyCandidate`; retaining graph-id-plus-edges comparison; trusting recomputed assessment ids without graph binding checks. + +**Rationale**: Direct graph APIs are public. Project rejects bad digests, replay validates assessments, and reconcile recomputes and compares the complete stored payload. + +## Data Flow + +```text +verifyCandidate + -> validateBindings + -> input.contract.contract_digest == executionGraph.contract_digest + -> normalize rawEvidence in caller-supplied list order + -> evidence/v2 (unchanged) + role/obligation/coverage binding metadata + -> evaluateStrategy + -> one EvidenceId cannot cover distinct strategy roles + -> bug: RED* < PATCH* < GREEN*; strict-tdd: RED* < GREEN* + -> walkMustObligations + -> union evidence_requirements_satisfied per obligation + -> required_evidence subset check + -> emit assessment/v1 for each contributing tuple + -> emit verification/v2 + -> projectAssuranceGraph + -> validate canonical digests -> canonical nodes/edges -> graph_id +``` + +```text +persisted candidate + graph + evidence + assessments + verification + -> replayAssuranceGraph + -> schema + assessment_id + candidate/policy/evidence/node/obligation checks + -> token-level MUST coverage check + -> projectAssuranceGraph + -> reconcileAssuranceGraph(stored, canonical input) + -> recompute stored graph_id from stored payload + -> compare full stored payload with current canonical projection + -> any mismatch: GRAPH_DIVERGENCE +``` + +All four resolved canonical values must be SHA-256 strings; supplied values must equal authoritative Graph/contract bindings. For coverage, the MUST walk intersects each binding's tokens with the obligation requirements, persists relevant sorted tokens, and checks their union. Missing tokens return `UNFULFILLED_MUST`; existing codes remain for unknown obligations, wrong nodes, and provenance failure. + +## File Changes + +| File | Action | Description | +| --- | --- | --- | +| `scripts/lib/independent-verifier/{bindings,evidence,strategy-policy}.js` | Modify | Contract gate; normalized coverage; role identity and sequence. | +| `scripts/lib/independent-verifier/{assessment,obligation-coverage,index}.js` | Modify | Canonical assessment validation, token-subset walk, ordered facade gates. | +| `scripts/lib/assurance-graph/{projector,index}.js` | Modify | Strict canonical inputs, assessment replay, full stored-graph reconciliation. | +| `schemas/kernel/assessment/v1.schema.json` | Modify | Require closed-array field `evidence_requirements_satisfied`. | +| `schemas/kernel/contract-claims.json` | Modify | Add the field to assessment required claims; leave manifest `$id` unchanged. | +| `schemas/kernel/assessment/fixtures/{valid,invalid}/*.json` | Modify/Create | Complete/four-role coverage; verdict, cross-family, and missing-coverage failures. | +| `scripts/lib/independent-verifier/*.test.js` | Modify | Identity, subset, aliasing, digest, and sequence adversaries. | +| `scripts/lib/assurance-graph/index.test.js` | Modify | Input, assessment, replay, and stored-payload tampering. | +| `scripts/lib/k6b-schema-fixtures.test.js` | Modify | Claims/fixtures and frozen-contract pins. | +| `scripts/k6b-verifier-assurance-graph-e2e.test.js` | Modify | Complete verify/project/replay/reconcile path. | +| `docs/architecture/harness-evolution.md` | Modify | Mark K6b `revise` and K6c blocked until remediation archive. | +| `docs/roadmaps/harness-evolution.md` | Modify | Apply the same temporary dependency status at roadmap checkpoints. | + +## Interfaces / Contracts + +```javascript +// Raw verifier binding metadata; evidence/v2 remains unchanged. +{ + role: "acceptance", + obligation_ids: ["req-repair-001"], + evidence_requirements_satisfied: ["ev:test-pass"], + bytes, origin, node_id +} + +// ospec://schemas/kernel/assessment/v1 +{ + schema_version: 1, + kind: "assessment/v1", + assessment_id, + evidence_id, + role, + obligation_id, + node_id, + candidate_id, + policy_snapshot_id, + evidence_requirements_satisfied: ["ev:test-pass"] +} +``` + +`computeAssessmentId` fingerprints every field above except `assessment_id`, with coverage sorted and deduplicated first. The assessment schema remains `additionalProperties: false` and forbids `verdict`. Direct schema validation of four same-evidence role records remains valid; `verifyCandidate` rejects their combined semantic use because schema validity is not strategy admissibility. + +## Requirement Allocation + +| Requirement/scenarios | Component allocation | +| --- | --- | +| REQ-independent-verification-008 digest mismatch | `bindings.js` before `selectStrategy`; facade test proves no verdict. | +| REQ-independent-verification-005 unknown obligation/wrong node/partial `[A,B]` coverage | `evidence.js` binding metadata + `obligation-coverage.js` subset walk. | +| REQ-independent-verification-006 aliasing and order adversaries | `strategy-policy.js`; `assessment.js` retains distinct tuple identities. | +| REQ-kernel-contract-schemas-027 complete/missing/cross-family/frozen pins | assessment schema/fixtures, contract claims, K6b schema fixture tests. | +| REQ-assurance-graph-007 contradiction/null digest | `projector.js` canonical binding validator before graph-id construction. | +| REQ-assurance-graph-006 tampered/schema/candidate/policy/evidence/obligation/node replay | `assessment.js` validator + `assurance-graph/index.js` replay preflight. | +| REQ-assurance-graph-008 nodes/canonical inputs/candidate/kind/schema/stored-id divergence | stored-payload graph-id recomputation and full canonical comparison in `assurance-graph/index.js`. | + +## Testing Strategy + +| Layer | What to Test | Approach | +| --- | --- | --- | +| Unit | Contract digest gate | C1 graph/C2 contract returns before strategy and emits no PASS. | +| Unit | Strategy semantics | Same id for distinct roles; GREEN before RED; RED after PATCH; valid ordered distinct ids. | +| Unit | Coverage and assessment identity | `[A,B]` with A only fails; multiple bindings union to `[A,B]`; coverage mutation changes id. | +| Contract | `assessment/v1` | Valid complete/four-role fixtures; omitted coverage, verdict, and cross-family fail; frozen pins unchanged. | +| Unit | Projector | Contradictory provided/authoritative digests and null resolved digest return `GRAPH_DIVERGENCE`. | +| Integration | Replay | Tamper schema, id, candidate, policy, evidence reference, obligation, node, or coverage and fail closed. | +| Integration | Reconcile | Tamper nodes, canonical inputs, candidate, kind/schema, or stored graph id and return `GRAPH_DIVERGENCE`. | +| E2E | Full K6b pipeline | Valid token-complete evidence verifies, projects twice deterministically, replays byte-identically, and rejects stored mutations. | + +## Migration / Rollout + +No data migration or feature flag. Existing partial `assessment/v1` payloads without coverage are intentionally invalid and must be regenerated from canonical verifier inputs; they are not upgraded in place. Apply schema, verifier, projector/replay/reconcile, fixtures, tests, and temporary roadmap status as one rollback unit. Archive may restore K6b `done` and K6c `next-eligible` only after verification. Frozen `evidence/v2`, `verification/v2`, and K1 baselines must remain byte-identical throughout. + +## Open Questions + +None. diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/prepared-specs/assurance-graph/spec.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/prepared-specs/assurance-graph/spec.md new file mode 100644 index 00000000..2c78bbde --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/prepared-specs/assurance-graph/spec.md @@ -0,0 +1,242 @@ +# Assurance Graph Specification + +## Purpose + +Define the Assurance Graph as a content-addressed, reproducible projection of +evidence and verification over a frozen Candidate. It derives from OpenSpec, +Git, Candidate, and Execution Graph. It is never semantic authority. Selective +invalidation on successor preserves independent evidence. + +## Requirements + +### Requirement:### Requirement: Derived Projection Not Authority {#REQ-assurance-graph-001} + +The Assurance Graph MUST be derived from canonical OpenSpec artifacts, Git +bytes, the frozen Candidate, the Execution Graph, evidence nodes, verification +decisions, and persistable assessments. Consumers MUST treat it as read-only. +The graph MUST NOT grant lifecycle, approval, or delivery authority. +Divergence from OpenSpec/Git/Candidate/contract/policy MUST fail closed with +`GRAPH_DIVERGENCE`. Failure to materialize the projection MUST fail closed +with `GRAPH_PROJECTION_FAILED`. K6b MUST NOT populate `reviewed-by` edges or +treat Evaluation Attestation or Delivery Authorization as authority. +(Previously: divergence was unnamed fail-closed and canonical inputs excluded contract/policy digest binding.) + +#### Scenario: Matching canonical inputs project a graph + +- GIVEN canonical OpenSpec/Git/Candidate/Execution Graph inputs and bound evidence +- WHEN the Assurance Graph is materialized +- THEN the projection MUST be derived from those inputs +- AND MUST NOT override OpenSpec/Git/Candidate + +#### Scenario: Divergent graph fails closed + +- GIVEN an Assurance Graph that cannot be recomputed from current OpenSpec/Git/Candidate +- WHEN reconciliation runs +- THEN validation MUST fail closed with `GRAPH_DIVERGENCE` +- AND consumers MUST NOT proceed on the unreconciliation graph + +#### Scenario: Contract or policy change forces reconciliation fail-closed + +- GIVEN a stored graph projected under contract digest C1 and policy snapshot P1 +- AND current canonical inputs are C2 or P2, producing a distinct `graph_id` +- WHEN reconciliation runs against the stored graph +- THEN validation MUST fail closed with `GRAPH_DIVERGENCE` + +### Requirement:### Requirement: Reproducible Digest And K6b Edges {#REQ-assurance-graph-002} + +Identical persistable canonical inputs MUST produce the same graph digest and +the same `AssuranceEdge` set. `graph_id` MUST fingerprint at least: + +| Canonical input | In `graph_id` preimage | +| --- | --- | +| contract digest | MUST | +| policy snapshot identity | MUST | +| Execution Graph digest | MUST | +| canonical OpenSpec input | MUST | +| `candidate_id` and canonical nodes/edges | MUST | + +Altering any of those inputs MUST produce a distinct `graph_id`. Edge +serialization order MUST NOT change the digest. Each K6b edge MUST have +`from`, `to`, and `relation` in +`verified-by | satisfies | derived-from | invalidates`. The projection MAY +include requirement, graph-node, work-order, source/patch, candidate, +test-evidence, and verification-decision subjects. It MUST NOT emit K7 +findings, K8 attestation, or K10 authorization as authoritative subjects. +(Previously: `graph_id` digested only candidate_id plus canonical nodes/edges.) + +#### Scenario: Same inputs yield the same digest and edges + +- GIVEN identical canonical inputs +- WHEN the Assurance Graph is materialized twice +- THEN both digests MUST be byte-identical +- AND both edge sets MUST be equal + +#### Scenario: Forbidden later-slice relations are rejected + +- GIVEN an edge with relation `reviewed-by` or a node claiming Evaluation Attestation authority +- WHEN the K6b graph is validated +- THEN validation MUST fail closed + +#### Scenario: Canonical input change yields a distinct graph_id + +- GIVEN a projection under contract digest C1, policy snapshot P1, Execution Graph digest G1, and canonical OpenSpec input O1 +- WHEN any one of C1, P1, G1, or O1 is replaced with a distinct persistable value +- THEN the resulting `graph_id` MUST differ +- AND reconciliation against the prior graph MUST fail closed with `GRAPH_DIVERGENCE` + +### Requirement:### Requirement: Selective Invalidation Closure {#REQ-assurance-graph-003} + +When a Candidate successor appears or a source subject changes, the system +MUST compute the dependent closure over `invalidates`, `derived-from`, +`verified-by`, and `satisfies` edges, invalidate dependent evidence, and +preserve evidence outside that closure. The system MUST NOT re-execute every +verification by default. The system MUST NOT reuse evidence reachable through +a transitive `invalidates` edge. + +#### Scenario: Successor invalidates only the dependent closure + +- GIVEN an Assurance Graph with dependent evidence D and independent evidence I +- AND a Candidate successor that affects only D's source subjects +- WHEN selective invalidation runs +- THEN D MUST be invalidated +- AND I MUST remain valid + +#### Scenario: Transitive invalidates blocks reuse + +- GIVEN evidence E connected to a successor-affected subject by a transitive `invalidates` path +- WHEN verification is attempted with E +- THEN the verifier MUST fail closed +- AND MUST NOT treat E as satisfying evidence + +### Requirement:### Requirement: Non-Authoritative Equivalence Manifest {#REQ-assurance-graph-004} + +The system MAY emit an equivalence manifest bound to the graph digest and +`CandidateId` for later K9 evaluation. The manifest MUST NOT promote +equivalence, alter Strict TDD fallback, or serve as attestation or delivery +authorization. + +#### Scenario: Manifest is emitted without promotion + +- GIVEN a reproducible Assurance Graph for a frozen Candidate +- WHEN an equivalence manifest is emitted +- THEN it MUST bind the graph digest and CandidateId +- AND MUST NOT change verifier fallback or authorize delivery + +#### Scenario: Manifest cannot alias attestation or authorization + +- GIVEN an equivalence manifest payload +- WHEN validated against CandidateEvaluationAttestation or DeliveryAuthorization schemas +- THEN validation MUST fail closed + +### Requirement:### Requirement: Forbidden Subjects Matched By Kind And Namespace {#REQ-assurance-graph-005} + +`rejectForbidden` MUST accept or reject subjects using structured `kind` and +`namespace` (or equivalent typed fields). It MUST NOT reject a subject because +a forbidden token is a substring of `id`. A node with `kind: "requirement"` +and `id: "REQ-add-authorization-header"` MUST remain valid. Nodes whose `kind` +or `namespace` denote K7 finding, K8 attestation, or K10 authorization MUST +fail closed. Edges whose `relation` is outside +`verified-by | satisfies | derived-from | invalidates` MUST fail closed. + +#### Scenario: Requirement id containing authorization remains valid + +- GIVEN a projected node `{ id: "REQ-add-authorization-header", kind: "requirement" }` +- WHEN `rejectForbidden` runs +- THEN validation MUST succeed +- AND MUST NOT reject the node for the substring `authorization` + +#### Scenario: Structured authorization kind is rejected + +- GIVEN a projected node whose `kind` or `namespace` denotes authorization, attestation, or finding +- WHEN `rejectForbidden` runs +- THEN validation MUST fail closed + +### Requirement: Replay From Persistable Outputs {#REQ-assurance-graph-006} + +The Assurance Graph MUST be reproducible from persistable outputs: canonical +input digests, projected nodes and edges, evidence records, verification +records, and assessment/binding records that carry `obligation_id`. Replay +MUST NOT require ephemeral projector fields. Consumers MUST NOT reinvent +`obligation_id` values from vanished fields. `satisfies` edges MUST be +rebuildable from persistable assessments. `replayAssuranceGraph` MUST +revalidate each persisted assessment before accepting the replay: schema +validity; recomputed assessment identity; `candidate_id` match; bound +policy-snapshot identity; referenced evidence exists; `obligation_id` is in +the graph; the bound node implements that obligation; and persistable +`node_id` matches the evidence record. Any check failure MUST fail closed +with `GRAPH_DIVERGENCE` or as an invalid assessment. A tampered +`assessment_id` MUST NOT replay as valid. +(Previously: replay compared recomputed graph_id and edges without revalidating persisted assessments.) + +#### Scenario: Replay from persisted outputs yields the same graph + +- GIVEN a previously projected graph plus persisted evidence, verification, assessments, and canonical input digests +- AND no ephemeral `obligation_ids` on the original projector call +- AND every persisted assessment passes revalidation +- WHEN the graph is replayed from those persistable outputs only +- THEN the recomputed `graph_id` and edge set MUST be byte-identical to the stored graph + +#### Scenario: Tampered assessment_id fails replay + +- GIVEN persisted assessments plus a stored graph that would otherwise replay +- AND one assessment whose `assessment_id` does not match the identity recomputed from its persistable fields +- WHEN `replayAssuranceGraph` runs +- THEN replay MUST fail closed with `GRAPH_DIVERGENCE` or as an invalid assessment + +#### Scenario: Assessment fails schema, candidate, or policy revalidation + +- GIVEN a persisted assessment that fails schema validation, whose `candidate_id` does not match the graph subject, or whose bound policy-snapshot identity does not match the graph +- WHEN `replayAssuranceGraph` runs +- THEN replay MUST fail closed with `GRAPH_DIVERGENCE` or as an invalid assessment + +#### Scenario: Assessment bound to missing evidence or non-implementing node fails replay + +- GIVEN a persisted assessment whose `evidence_id` does not exist, whose `obligation_id` is absent from the graph, whose node does not implement that obligation, or whose `node_id` disagrees with the evidence record +- WHEN `replayAssuranceGraph` runs +- THEN replay MUST fail closed with `GRAPH_DIVERGENCE` or as an invalid assessment + +### Requirement: Projector Fail-Closed On Contradictory Canonical Inputs {#REQ-assurance-graph-007} + +`projectAssuranceGraph` MUST fail closed with `GRAPH_DIVERGENCE` when supplied +`canonicalInputs` contradict the Graph, contract, or policy they claim to +project. Required canonical input digests MUST be present and non-null. The +projector MUST NOT fingerprint a null or absent digest for any required +canonical input in the `graph_id` preimage. Contradiction or a null required +digest MUST NOT yield a successful projection. + +#### Scenario: Graph contract contradicts canonicalInputs + +- GIVEN an Execution Graph whose contract digest is C1 +- AND `canonicalInputs` whose contract digest is C2, where C1 ≠ C2 +- WHEN `projectAssuranceGraph` runs +- THEN projection MUST fail closed with `GRAPH_DIVERGENCE` +- AND MUST NOT emit a graph whose `graph_id` fingerprints the contradictory pair + +#### Scenario: Null required canonical digest is not fingerprinted + +- GIVEN a required canonical input whose digest is null or absent +- WHEN `projectAssuranceGraph` runs +- THEN projection MUST fail closed +- AND MUST NOT produce a `graph_id` that fingerprints a null digest for that input + +### Requirement: Reconcile Stored Payload Divergence {#REQ-assurance-graph-008} + +`reconcileAssuranceGraph` MUST detect stored-graph divergence beyond equality +of `graph_id` and edges. It MUST fail closed with `GRAPH_DIVERGENCE` when +stored `nodes`, `canonical_inputs`, `candidate_id`, or kind/schema diverge +from the recomputed projection, or when `graph_id` recomputed from the stored +payload does not match the stored `graph_id`. Comparing only stored +`graph_id` plus edges MUST NOT be sufficient to declare reconciliation OK. + +#### Scenario: Tampered stored nodes fail closed + +- GIVEN a stored Assurance Graph whose `graph_id` and edges match a recomputation +- AND whose persistable `nodes` have been altered +- WHEN `reconcileAssuranceGraph` runs +- THEN validation MUST fail closed with `GRAPH_DIVERGENCE` + +#### Scenario: Stored payload or identity fields diverge + +- GIVEN a stored Assurance Graph whose `candidate_id`, persistable `canonical_inputs`, or kind/schema differ from the current projection, or whose declared `graph_id` does not equal the digest recomputed from its stored payload +- WHEN `reconcileAssuranceGraph` runs +- THEN validation MUST fail closed with `GRAPH_DIVERGENCE` diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/prepared-specs/independent-verification/spec.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/prepared-specs/independent-verification/spec.md new file mode 100644 index 00000000..c8d86552 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/prepared-specs/independent-verification/spec.md @@ -0,0 +1,281 @@ +# Independent Verification Specification + +## Purpose + +Independent verifier over a frozen `CandidateId`. Worker narrative is not +authority. Evidence stays distinct from verdicts. Strict TDD is the default +evidence strategy until equivalence is proven; it does not rewrite SDD +`testing.tdd_mode`. + +## Requirements + +### Requirement:### Requirement: Frozen Candidate Subject And Canonical Bindings {#REQ-independent-verification-001} + +The verifier MUST accept only a frozen `CandidateId` as subject, plus contract, +Execution Graph, repository bytes, and raw evidence. It MUST validate canonical +identity bindings before evaluating evidence. It MUST reject a `WorkResult` +(integrated or not) as subject and MUST reject an unfrozen or mutable +candidate. Binding failure MUST fail closed. Worker prose MUST NOT substitute +for a missing structured subject or binding. + +#### Scenario: Frozen CandidateId proceeds to strategy selection + +- GIVEN a frozen Candidate v2, valid Execution Graph and identity bindings, and raw evidence +- WHEN the verifier starts +- THEN it MUST accept the `CandidateId` as subject and proceed to strategy selection + +#### Scenario: WorkResult subject is rejected + +- GIVEN a `WorkResultId` or unintegrated WorkResult supplied as the verification subject +- WHEN the verifier starts +- THEN verification MUST fail closed and MUST NOT treat the WorkResult as a Candidate + +#### Scenario: Unfrozen candidate or failed binding is rejected + +- GIVEN an unfrozen candidate, a mutable working tree claimed as candidate, or a binding digest mismatch +- WHEN the verifier starts +- THEN verification MUST fail closed before strategy evaluation + +### Requirement:### Requirement: Evidence Strategies And Strict TDD Fallback {#REQ-independent-verification-002} + +The verifier MUST select exactly one strategy from +`bug | feature | refactor | migration | config-docs`. Each strategy MUST +declare minimum evidence, at least one negative case, and admissible +provenance: + +| Strategy | Minimum evidence | Required negative | +| --- | --- | --- | +| bug | red reproduction, patch, green reproduction | green-without-red or red-after-patch | +| feature | acceptance, invariants, contract or integration tests | missing negative or acceptance path | +| refactor | characterization before/after, no observable behavior change | behavioral delta treated as equivalent | +| migration | dry-run, rollback, incompatibility, idempotent re-run | skipped rollback or non-idempotent apply | +| config-docs | real schema/parser check, smoke, install or consume | docs-only claim without parser/smoke | + +When no strategy is declared, or no proven equivalent applies, the verifier +MUST use Strict TDD (RED → GREEN with runtime test evidence) as the default +fallback. This fallback MUST NOT rewrite `openspec/config.yaml` +`testing.tdd_mode` (this repository remains `focused` unless a separate change +updates it). Strict TDD fallback is an evidence-strategy default, not a silent +SDD-runtime config mutation. An equivalence manifest MUST NOT retire the +fallback. + +#### Scenario: Declared feature strategy requires its minimums + +- GIVEN a frozen Candidate declared as `feature` +- WHEN the verifier selects the feature strategy +- THEN it MUST require the feature minimum set and a negative case +- AND MUST NOT accept characterization-only evidence + +#### Scenario: Missing strategy falls back to Strict TDD without rewriting tdd_mode + +- GIVEN a frozen Candidate with no declared strategy and no proven equivalence +- AND `openspec/config.yaml` `testing.tdd_mode` is `focused` +- WHEN the verifier selects a strategy +- THEN it MUST apply Strict TDD as the evidence strategy +- AND MUST leave `testing.tdd_mode` unchanged + +### Requirement:### Requirement: Provenance Sufficiency And Fail-Closed Evidence {#REQ-independent-verification-003} + +Every evidence node MUST declare provenance as exactly one of +`runtime-observed | host-attested | tool-produced | model-reported | +human-decision | external-unverified`. Policy MUST decide which classes MAY +satisfy each obligation. Strong classes (`runtime-observed`, `host-attested`, +`tool-produced`) MUST be derived from trusted collector or transport channel +metadata. The verifier MUST NOT accept a strong class solely because the raw +payload string claims it. Payload digest MUST NOT be treated as origin. When +collector or transport metadata is absent, untrusted, or disagrees with a +claimed strong class, sufficiency MUST fail closed. PKI MUST NOT be required. +Evidence that is insufficient, stale relative to the frozen Candidate, bound +to a foreign subject, or fabricated MUST fail closed. A worker +`model-reported` claim MUST NOT satisfy an obligation that requires a strong +class. +(Previously: strong provenance could be taken from the raw payload provenance string.) + +#### Scenario: Runtime-observed evidence satisfies a test obligation + +- GIVEN an obligation that admits `runtime-observed` provenance +- AND collector or transport metadata that derives `runtime-observed` +- AND raw test evidence bound to the frozen CandidateId and graph node +- WHEN the verifier evaluates sufficiency +- THEN the obligation MAY be marked satisfied +- AND the evidence MUST remain a distinct record without `verdict` + +#### Scenario: Model-reported tests-passed is insufficient + +- GIVEN an obligation that requires `runtime-observed` or `tool-produced` provenance +- AND only a worker `model-reported` claim that tests passed +- WHEN the verifier evaluates sufficiency +- THEN verification MUST fail closed +- AND MUST NOT treat worker narrative as satisfying evidence + +#### Scenario: Stale, foreign, or fabricated evidence is rejected + +- GIVEN evidence whose digest does not match raw bytes, whose CandidateId does not match the subject, or whose origin predates a successor without revalidation +- WHEN the verifier evaluates that evidence +- THEN verification MUST fail closed +- AND MUST NOT reuse the evidence under a transitive `invalidates` edge + +#### Scenario: Payload-claimed strong provenance without trusted collector fails closed + +- GIVEN raw evidence whose payload `provenance` is `runtime-observed` +- AND collector or transport metadata that is absent, untrusted, or derives a weaker class +- WHEN the verifier evaluates sufficiency +- THEN verification MUST fail closed +- AND MUST NOT accept the payload string as a strong class + +### Requirement:### Requirement: Verdict Is Not Evidence {#REQ-independent-verification-004} + +Verification MUST emit a verification record bound to the frozen `CandidateId` +with verdict `PASS | PASS WITH WARNINGS | FAIL`. Evidence records MUST NOT +carry `verdict`. A verification record MUST NOT validate as evidence. `PASS` +or `PASS WITH WARNINGS` MUST require both declared-strategy minimums and +REQ-independent-verification-005 coverage with persistable assessments. +Strategy role shape alone is insufficient. Unique-sort of `evidence_ids` MUST +NOT substitute for distinct assessments. The verifier MAY emit a +non-authoritative equivalence manifest for later K9 evaluation. That manifest +MUST NOT promote equivalence, authorize delivery, or replace the Strict TDD +fallback. +(Previously: strategy minimums with admissible provenance were sufficient for PASS.) + +#### Scenario: Sufficient evidence yields a verification verdict + +- GIVEN strategy minimums met with collector-derived admissible provenance on a frozen CandidateId +- AND every applicable non-deferred MUST obligation persistably bound to admissible evidence on a correct implementing node +- WHEN the verifier completes +- THEN it MUST emit a verification record with `PASS` or `PASS WITH WARNINGS` +- AND referenced evidence records MUST omit `verdict` + +#### Scenario: Evidence carrying verdict is rejected + +- GIVEN a payload that mixes evidence identity fields with a `verdict` property +- WHEN schema or verifier validation runs +- THEN validation MUST fail closed +- AND MUST NOT accept the payload as either evidence or verification + +### Requirement: Obligation Manifest MUST Coverage {#REQ-independent-verification-005} + +After strategy evaluation, the verifier MUST walk every Obligation Manifest +item with criticality `must` that is not an approved `deferred` record +(`reason` and `approved_by`). For each such obligation, a `PASS` or +`PASS WITH WARNINGS` verdict MUST require `required_evidence` ⊆ persistable +satisfied tokens, each persistably bound to that `obligation_id` and to a +`node_id` listed in `implemented_by`. Coverage MUST be persisted on the +assessment/binding record (additive field; exact name design-owned). A +non-empty evidence list, unique-sort of `evidence_ids`, or existential +binding MUST NOT substitute for that subset. Strategy role shape alone MUST +NOT satisfy the graph. An `obligation_id` absent from the manifest MUST fail +closed. Evidence bound to a node that does not implement the obligation MUST +fail closed. A MUST whose `required_evidence` is not a subset of satisfied +tokens MUST fail closed and MUST identify the unfulfilled `obligation_id`. +The verifier MUST consume persistable manifest `obligation_id` values; it +MUST NOT invent them from vanished fields. +(Previously: coverage required admissible evidence "covering" required_evidence without subset semantics or persistable satisfied tokens.) + +#### Scenario: MUST without admissible evidence fails closed + +- GIVEN a compiled Execution Graph with a non-deferred MUST obligation +- AND strategy role minimums met +- AND no admissible evidence persistably bound to that `obligation_id` +- WHEN the verifier evaluates obligation coverage +- THEN it MUST fail closed identifying that `obligation_id` +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +#### Scenario: Nonexistent obligation_id fails closed + +- GIVEN an assessment or raw binding whose `obligation_id` is not in the Obligation Manifest +- WHEN the verifier evaluates obligation coverage +- THEN it MUST fail closed identifying the unknown `obligation_id` +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +#### Scenario: Evidence bound to the wrong implementing node fails closed + +- GIVEN a MUST obligation with `implemented_by` containing node A +- AND admissible evidence whose persistable `node_id` is B, where B is not in `implemented_by` +- WHEN the verifier evaluates that binding +- THEN it MUST fail closed +- AND MUST NOT treat the evidence as satisfying that obligation + +#### Scenario: Partial required_evidence coverage fails closed + +- GIVEN a non-deferred MUST obligation with `required_evidence` equal to `[A, B]` +- AND admissible evidence that satisfies only token A +- WHEN the verifier evaluates obligation coverage +- THEN it MUST fail closed identifying that `obligation_id` +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +### Requirement: Persistable Assessment Binding Distinct From Evidence {#REQ-independent-verification-006} + +The verifier MUST persist an additive assessment/binding record for each +evaluated tuple of `evidence_id`, `role`, `obligation_id`, `node_id`, and bound +policy-snapshot identity. Assessment identity MUST include `role` and +`obligation_id` and MUST be distinct per `(evidence_id, role, obligation_id)`. +`evidence/v2` MUST remain the observation record and MUST NOT be mutated to +carry `role` or `obligation_id`. Incompatible strategy roles MUST NOT share +one EvidenceId. The same observation MUST NOT satisfy incompatible roles. +Strict TDD evidence is a RED → GREEN sequence, not a set of role labels: +GREEN-before-RED MUST fail closed, and RED-after-PATCH MUST fail closed. +Unique-sort of `verification.evidence_ids` MUST NOT be the assessment identity +and MUST NOT hide distinct role or obligation bindings. +(Previously: the same EvidenceId used as four roles was required to yield four passing assessments.) + +#### Scenario: Same EvidenceId as RED and GREEN fails closed + +- GIVEN one `evidence/v2` observation whose `evidence_id` is E +- AND that observation is bound as both RED and GREEN strategy roles +- WHEN the verifier evaluates strategy evidence +- THEN verification MUST fail closed +- AND MUST NOT treat E as satisfying both roles + +#### Scenario: GREEN before RED fails closed + +- GIVEN Strict TDD or bug-strategy evidence whose GREEN observation precedes RED +- WHEN the verifier evaluates strategy evidence +- THEN verification MUST fail closed +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +#### Scenario: RED after PATCH fails closed + +- GIVEN Strict TDD or bug-strategy evidence whose RED observation occurs after PATCH +- WHEN the verifier evaluates strategy evidence +- THEN verification MUST fail closed +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +#### Scenario: Distinct tuples yield distinct assessment identities + +- GIVEN two persistable tuples that differ in `evidence_id`, `role`, or `obligation_id` +- AND neither tuple shares one EvidenceId across incompatible roles +- WHEN assessments are persisted +- THEN their assessment identities MUST be distinct +- AND unique-sort of `verification.evidence_ids` MUST NOT collapse those assessments + +### Requirement:### Requirement: Facade Fail-Closed On Required Projection {#REQ-independent-verification-007} + +`verifyCandidate` MUST require a successful Assurance Graph projection before +returning `ok: true`. If projection cannot materialize, the facade MUST fail +closed with `GRAPH_PROJECTION_FAILED`. If a stored graph does not recompute +from persistable canonical inputs, the facade MUST fail closed with +`GRAPH_DIVERGENCE`. The facade MUST NOT return `ok: true` without the projected +graph, and MUST NOT emit `PASS` or `PASS WITH WARNINGS` in those cases. + +#### Scenario: Failed projection does not return ok without a graph + +- GIVEN a candidate whose strategy and MUST coverage would otherwise pass +- AND `projectAssuranceGraph` fails +- WHEN `verifyCandidate` completes +- THEN the facade MUST return `ok: false` with `GRAPH_PROJECTION_FAILED` +- AND MUST omit `assurance_graph` +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +### Requirement: Contract Digest Gate Before Strategy {#REQ-independent-verification-008} + +Before strategy evaluation, `input.contract.contract_digest` MUST equal +`executionGraph.contract_digest`. Mismatch MUST fail closed and MUST NOT +proceed to strategy evaluation. + +#### Scenario: Contract digest mismatch with Execution Graph fails closed before strategy + +- GIVEN a frozen Candidate whose `input.contract.contract_digest` is C2 +- AND an Execution Graph whose `contract_digest` is C1, where C1 ≠ C2 +- WHEN the verifier validates canonical bindings +- THEN verification MUST fail closed before strategy evaluation +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/prepared-specs/kernel-contract-schemas/spec.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/prepared-specs/kernel-contract-schemas/spec.md new file mode 100644 index 00000000..f27b823f --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/prepared-specs/kernel-contract-schemas/spec.md @@ -0,0 +1,856 @@ +# kernel-contract-schemas Specification + +## Purpose + +Define the versioned kernel contract suite: JSON Schemas with `$id`/version, +valid/invalid fixtures, aliases, and migration rules that preserve existing +consumer tags — without executing the lifecycle reducer. + +## Requirements + +### Requirement:### Requirement: Versioned Schema Families With Id And Version {#REQ-kernel-contract-schemas-001} + +The contract suite MUST publish a versioned JSON Schema for each family: state/transition, classification, contract, graph/node, work order/result, candidate, SourceSnapshot, WorkOrder, WorkResult, Candidate, evidence, verification, finding/review, failure/recovery, receipt, event, OperationPermit, OperationReceipt, effect-class, HostCapabilities, HostAdapter, ExecutionTransport, QuestionTransport, WorkerTransport, ToolExecutionTransport, DeliveryGateTransport, CapabilityProof, transport-request, transport-outcome, transport-failure, execution-graph, policy-snapshot, clarify-event, execution-budget, authority-effect-budget, causal-failure, failure-recovery-transition, workspace-descriptor, capsule-definition, work-result-execution-payload, containment-violation, assurance-graph, and assessment/binding. Every schema MUST declare a stable `$id` and an explicit version field (`schema_version` or equivalent). Consumers MUST be able to pin a schema by `$id`/version. +(Previously: K6b closed the inventory at assurance-graph; this remediation adds an additive assessment/binding family without mutating evidence/v2, verification/v2, or K1 v1 pins.) + +#### Scenario: Every required family has $id and version + +- GIVEN the published contract suite +- WHEN each required schema family is inspected +- THEN the schema MUST expose a non-empty `$id` +- AND MUST expose an explicit version identifier + +#### Scenario: Consumer can pin a schema version + +- GIVEN a schema family published at version N +- WHEN a consumer references that family's `$id` and version N +- THEN resolution MUST return the schema for version N +- AND MUST NOT silently substitute a different version + +#### Scenario: K2.1 families are included in the required set + +- GIVEN the required schema family inventory +- WHEN OperationPermit, OperationReceipt and effect-class are checked +- THEN each MUST be present as a pinned versioned family + +#### Scenario: K2a families are included in the required set + +- GIVEN the required schema family inventory +- WHEN HostCapabilities, HostAdapter, the five transports and CapabilityProof + are checked +- THEN each MUST be present as a pinned versioned family + +#### Scenario: k2a-1 transport envelope families are included + +- GIVEN the required schema family inventory +- WHEN transport-request, transport-outcome, and transport-failure are checked +- THEN each MUST be present as a pinned versioned family + +#### Scenario: K3 execution identity families are included in the required set + +- GIVEN the required schema family inventory +- WHEN SourceSnapshot, WorkOrder, WorkResult, and Candidate identity schemas are checked +- THEN each MUST be present as a pinned versioned family with distinct $id + +#### Scenario: K4a execution graph, policy snapshot, and clarify event families are included in the required set + +- GIVEN the required schema family inventory +- WHEN execution-graph, policy-snapshot, and clarify-event schemas are checked +- THEN each MUST be present as a pinned versioned family with a distinct non-empty $id + +#### Scenario: K5 budget and failure recovery families are included in the required set + +- GIVEN the required schema family inventory +- WHEN execution-budget, authority-effect-budget, causal-failure, and failure-recovery-transition schemas are checked +- THEN each MUST be present as a pinned versioned family with a distinct non-empty $id + +#### Scenario: K6a worker isolation and containment families are included in the required set + +- GIVEN the required schema family inventory +- WHEN workspace-descriptor, capsule-definition, work-result-execution-payload, and containment-violation schemas are checked +- THEN each MUST be present as a pinned versioned family with a distinct non-empty $id + +#### Scenario: K6b assurance-graph family is included in the required set + +- GIVEN the required schema family inventory +- WHEN assurance-graph is checked +- THEN it MUST be present as a pinned versioned family with a distinct non-empty `$id` +- AND evidence/v2 and verification/v2 MUST be pinnable without mutating K1 evidence/v1 or verification/v1 + +#### Scenario: Assessment/binding family is included without mutating K6b pins + +- GIVEN the required schema family inventory +- WHEN assessment/binding is checked +- THEN it MUST be present as a pinned versioned family with a distinct non-empty `$id` +- AND evidence/v2, verification/v2, and K1 v1 pins MUST remain byte-identical + +### Requirement:### Requirement: Valid And Invalid Fixtures Per Schema Family {#REQ-kernel-contract-schemas-002} + +Each schema family MUST ship at least one fixture that validates successfully +and at least one fixture that MUST be rejected. Fixtures MUST exercise +required fields and at least one material negative case (missing required +field, wrong type, or forbidden extra authority field as applicable). + +#### Scenario: Valid fixture passes + +- GIVEN a schema family and its valid fixture +- WHEN the fixture is validated against the schema +- THEN validation MUST succeed + +#### Scenario: Invalid fixture fails + +- GIVEN a schema family and its invalid fixture +- WHEN the fixture is validated against the schema +- THEN validation MUST fail +- AND the failure MUST identify the violating path or rule + +--- + +### Requirement:### Requirement: Versioned Aliases Preserve Existing Tags {#REQ-kernel-contract-schemas-003} + +The suite MUST provide versioned aliases that map legacy or current stable +codes/tags to the canonical vocabulary. Migration rules MUST preserve existing +tags: a previously emitted stable tag MUST remain resolvable to an equivalent +canonical code after migration. Aliases MUST NOT silently drop or rename a +known consumer-facing tag without an explicit mapping entry. + +#### Scenario: Legacy tag resolves through alias + +- GIVEN a legacy stable tag that existing consumers emit +- AND a versioned alias mapping that tag to a canonical code +- WHEN migration/alias resolution runs +- THEN the legacy tag MUST resolve to the mapped canonical code +- AND MUST NOT be reported as unknown solely because of the rename + +#### Scenario: Unmapped tag is not silently dropped + +- GIVEN a known consumer-facing tag with no alias or migration entry +- WHEN migration/alias resolution runs under a strict mode that requires + coverage of known tags +- THEN resolution MUST fail closed or report the unmapped tag +- AND MUST NOT drop the tag silently + +--- + +### Requirement:### Requirement: Graph And Work-Order Shapes Are Consumable Contracts Only {#REQ-kernel-contract-schemas-004} + +Graph/node and work-order/result schemas MUST be published as consumable +contracts for later kernel work (K2–K4). Publishing those schemas MUST NOT +activate Graph IR as authority and MUST NOT implement or invoke a lifecycle +reducer. + +#### Scenario: Graph schema exists without reducer activation + +- GIVEN graph/node schemas are published with fixtures +- WHEN the repository is checked for K1 deliverables +- THEN the schemas and fixtures MUST be present and validatable +- AND no lifecycle reducer execution path MUST be introduced solely by those + schemas + +--- + +### Requirement:### Requirement: Schema Docs Cannot Name Unemitted Fields Or Commands {#REQ-kernel-contract-schemas-005} + +Contract documentation and fixtures that claim to describe emitted surfaces +MUST NOT name a field, operation, or command that the emitting code does not +produce. Such mismatches MUST be detectable by contract validation (enforced +via `contract-lint` checkers). + +#### Scenario: Fixture field absent from emitter is rejected + +- GIVEN a fixture or contract doc that names field `F` as emitted +- AND the emitter under test never produces `F` +- WHEN emission/contract validation runs +- THEN the check MUST report an offender for `F` + +--- + +### Requirement:### Requirement: Permit Receipt And Effect-Class Contract Families {#REQ-kernel-contract-schemas-006} + +The contract suite MUST publish versioned JSON Schemas for `OperationPermit`, +`OperationReceipt`, and effect-class (or effect-intent class) contracts. Each +family MUST declare a stable `$id` and explicit version. `OperationReceipt` +MUST use a distinct kind/schema from `receipt/v1` and MUST NOT be an alias of +Candidate Evaluation Attestation or Delivery Authorization schemas. + +#### Scenario: New families expose $id and version + +- GIVEN the published K2.1 contract suite +- WHEN OperationPermit, OperationReceipt and effect-class schemas are inspected +- THEN each MUST expose a non-empty `$id` +- AND MUST expose an explicit version identifier + +#### Scenario: OperationReceipt is not receipt/v1 + +- GIVEN the OperationReceipt schema and the existing receipt/v1 schema +- WHEN their `$id`/kind identifiers are compared +- THEN they MUST be distinct +- AND OperationReceipt MUST NOT resolve as an alias of receipt/v1 + +#### Scenario: Valid and invalid permit fixtures + +- GIVEN OperationPermit valid and invalid fixtures +- WHEN each is validated against the permit schema +- THEN the valid fixture MUST succeed +- AND the invalid fixture MUST fail identifying the violating path or rule + +--- + +### Requirement:### Requirement: Effect Class Enumeration Is Closed {#REQ-kernel-contract-schemas-007} + +Effect-class contracts MUST accept only +`pure | idempotent-keyed | probeable | compensatable | irreversible`. Any other +value MUST fail schema validation. + +#### Scenario: Unknown effect class is rejected + +- GIVEN an effect-class fixture with value `exactly-once` +- WHEN schema validation runs +- THEN validation MUST fail +- AND the failure MUST identify the class field + +--- + +### Requirement:### Requirement: Host And Capability-Proof Contract Families {#REQ-kernel-contract-schemas-008} + +The contract suite MUST publish versioned JSON Schemas for `HostCapabilities`, +`HostAdapter`, each of the five transport contracts, and `CapabilityProof`. +Each family MUST declare a stable `$id` and explicit version. +`CapabilityProof` and host/transport contracts MUST use distinct kinds/schemas +from `receipt/v1` and `OperationReceipt`, and MUST NOT alias Candidate +Evaluation Attestation or Delivery Authorization schemas. + +#### Scenario: Host families expose $id and version + +- GIVEN the published K2a contract suite +- WHEN HostCapabilities, HostAdapter, transport, and CapabilityProof schemas + are inspected +- THEN each MUST expose a non-empty `$id` +- AND MUST expose an explicit version identifier + +#### Scenario: CapabilityProof is not receipt/v1 or OperationReceipt + +- GIVEN CapabilityProof schema beside receipt/v1 and OperationReceipt +- WHEN their `$id`/kind identifiers are compared +- THEN CapabilityProof MUST be distinct from both +- AND MUST NOT resolve as an alias of either + +#### Scenario: Valid and invalid HostCapabilities fixtures + +- GIVEN HostCapabilities valid and invalid fixtures +- WHEN each is validated against the schema +- THEN the valid fixture MUST succeed +- AND the invalid fixture MUST fail identifying the violating path or rule + +--- + +### Requirement:### Requirement: Capability State Enumeration Is Closed {#REQ-kernel-contract-schemas-009} + +Host capability-state fields MUST accept only +`enforced | partial | instructional | unavailable`. Any other value MUST fail +schema validation. + +#### Scenario: Unknown capability state is rejected + +- GIVEN a HostCapabilities fixture with state `enabled` +- WHEN schema validation runs +- THEN validation MUST fail +- AND the failure MUST identify the state field + +--- + +### Requirement:### Requirement: CapabilityProof Required Fields Are Closed {#REQ-kernel-contract-schemas-010} + +CapabilityProof schemas MUST require `adapter_version`, `host_version`, +`fixture`, and `evidence_digest` as non-empty fields. Omitting any required +field MUST fail schema validation. + +#### Scenario: Incomplete proof fixture fails + +- GIVEN a CapabilityProof fixture missing `fixture` +- WHEN schema validation runs +- THEN validation MUST fail +- AND the failure MUST identify the missing required field + +--- + +### Requirement:### Requirement: Transport Request Outcome And Failure Families {#REQ-kernel-contract-schemas-011} + +The contract suite MUST publish additive versioned JSON Schema families +`transport-request`, `transport-outcome`, and `transport-failure` at v1. Each +family MUST declare a stable `$id` and explicit version distinct from the five +existing transport port schemas. Existing transport v1 `$id`s MUST remain +pinned and MUST NOT be silently mutated. Each new family MUST ship at least one +valid and one invalid fixture. + +`transport-request` MUST allow `requestId` and MAY carry `AbortSignal`/deadline +descriptors as schema-representable fields. `transport-outcome` MUST distinguish +success (`ok: true`) from failure. `transport-failure` MUST require `ok: false` +and a stable failure class covering at least timeout, cancel, reject, interrupt, +and worker-fail. + +#### Scenario: Additive families expose $id and version + +- GIVEN the published contract suite after this change +- WHEN transport-request, transport-outcome, and transport-failure schemas are + inspected +- THEN each MUST expose a non-empty `$id` +- AND MUST expose an explicit version identifier +- AND MUST NOT alias an existing transport port `$id` + +#### Scenario: Existing transport v1 ids remain unchanged + +- GIVEN the five existing transport port schemas published at v1 +- WHEN their `$id` values are compared to the pre-change pins +- THEN each `$id` MUST remain identical +- AND MUST NOT be silently rewritten by the additive families + +#### Scenario: Valid and invalid transport-failure fixtures + +- GIVEN transport-failure valid and invalid fixtures +- WHEN each is validated against the schema +- THEN the valid fixture MUST succeed +- AND the invalid fixture MUST fail identifying the violating path or rule + +#### Scenario: Outcome success cannot claim ok false simultaneously + +- GIVEN a transport-outcome fixture with contradictory ok/success markers +- WHEN schema validation runs +- THEN validation MUST fail closed + +--- + +### Requirement:### Requirement: Execution Identity Schemas With Non-Aliasing Fixtures {#REQ-kernel-contract-schemas-012} + +The contract suite MUST publish versioned JSON Schemas for `SourceSnapshot`, `WorkOrder`, `WorkResult`, and `Candidate` execution identity families. Each identity schema MUST declare a stable `$id` and explicit version field. The suite MUST define `candidate/v2.schema.json` (`$id: "ospec://schemas/kernel/candidate/v2"`) and `work-order/v2.schema.json` (`$id: "ospec://schemas/kernel/work-order/v2"`) with explicit `kind` field (`"candidate/v2"` and `"work-order/v2"`). WorkOrder v2 MUST require `source_snapshot_id` matching exactly `sha256:<64 lowercase hexadecimal characters>`; valid values MUST be preserved exactly by validation and consumers MUST NOT accept an absent, malformed, normalized, or substituted value. WorkOrder v2 `dependencies` array items MUST match pattern `^sha256:[a-f0-9]{64}$` representing canonical `WorkOrderId` sha256 digests of upstream prerequisite work orders. Candidate v2 MUST require `relation` and constrain it exactly to `exact`, `changed`, `ambiguous`, or `unknown`; it MUST allow nullable `predecessor_id` only as lineage metadata and MUST NOT retain aliases or retired relation values. `source-snapshot/v1.schema.json` and `work-result/v1.schema.json` MUST permit an optional `kind` property matching `"source-snapshot/v1"` and `"work-result/v1"` respectively without violating `additionalProperties: false`. Baseline `candidate/v1.schema.json`, `work-order/v1.schema.json`, and `K1_SCHEMA_BASELINE` MUST remain byte-identical immutable contracts. The suite MUST NOT retarget K1 pins to match altered v1 contents. Each identity family MUST include valid fixtures and negative non-aliasing fixtures demonstrating that `WorkResult` cannot validate as `Candidate`, and `Candidate` cannot validate as `CandidateEvaluationAttestation` or `DeliveryAuthorization`. Candidate fixtures MUST additionally prove rejection of retired relation vocabulary, impossible predecessor/relation combinations, commit projection, symlink changes, case-distinct paths, and projection changes. +(Previously: WorkOrder v2 dependencies items were unconstrained strings without sha256 digest pattern enforcement.) + +#### Scenario: K3 identity families expose stable id and version + +- GIVEN the published schemas for SourceSnapshot, WorkOrder, WorkResult, and Candidate +- WHEN each schema is inspected +- THEN each MUST expose a non-empty stable `$id` +- AND MUST expose an explicit version identifier + +#### Scenario: Identity confusion negative fixtures fail validation + +- GIVEN negative fixtures cross-substituting WorkResult, Candidate, CandidateEvaluationAttestation, and DeliveryAuthorization structures +- WHEN schema validation runs for each family +- THEN validation MUST fail closed +- AND the failure MUST identify the schema kind or identifier mismatch + +#### Scenario: Schema v2 exposes explicit kind discriminator for candidate and work-order + +- GIVEN a candidate/v2 or work-order/v2 JSON payload +- WHEN validated against `candidate/v2.schema.json` or `work-order/v2.schema.json` +- THEN the payload MUST contain property `kind` matching `"candidate/v2"` or `"work-order/v2"` respectively +- AND payloads lacking `kind` or carrying invalid `kind` values MUST be rejected fail-closed + +#### Scenario: WorkOrder v2 requires and preserves a valid source snapshot identifier + +- GIVEN a WorkOrder v2 payload with `source_snapshot_id` matching `sha256:<64 lowercase hexadecimal characters>` +- WHEN it is validated and consumed +- THEN validation MUST succeed +- AND the consumed identifier MUST equal the supplied identifier byte-for-byte + +#### Scenario: WorkOrder v2 requires dependencies items to match sha256 digest pattern + +- GIVEN a WorkOrder v2 payload with `dependencies` array containing strings not matching `^sha256:[a-f0-9]{64}$` +- WHEN validated against `work-order/v2.schema.json` +- THEN validation MUST fail closed identifying the invalid dependency pattern + +#### Scenario: WorkOrder v2 rejects absent or malformed source snapshot identifier + +- GIVEN a WorkOrder v2 payload with an absent, empty, uppercase, shortened, or otherwise malformed `source_snapshot_id` +- WHEN schema validation runs +- THEN validation MUST fail closed +- AND no alternate identifier MAY be inferred or substituted + +#### Scenario: Candidate v2 rejects retired relation and inconsistent successor fixture + +- GIVEN Candidate v2 fixtures using `superset` or a distinct predecessor with relation `exact` +- WHEN validated against the Candidate v2 schema and K3 contract fixture suite +- THEN each fixture MUST fail closed +- AND the failure MUST identify relation vocabulary or lineage coherence + +#### Scenario: Legacy v1 schemas and K1 baseline remain byte-identical and immutable + +- GIVEN `candidate/v1.schema.json`, `work-order/v1.schema.json`, and `K1_SCHEMA_BASELINE` pins +- WHEN verified against repository schema baseline rules +- THEN the v1 schemas and K1 pins MUST remain byte-identical to their frozen baseline +- AND validation MUST reject any pin-only retarget that masks changed v1 content + +#### Scenario: Legacy WorkOrder v1 fixtures remain valid alongside v2 + +- GIVEN valid pre-existing `work-order/v1` fixtures and valid WorkOrder v2 fixtures +- WHEN each fixture is validated against its pinned schema +- THEN each v1 fixture MUST remain valid under `work-order/v1` +- AND each v2 fixture MUST validate only under `work-order/v2` + +#### Scenario: SourceSnapshot v1 and WorkResult v1 allow optional kind property + +- GIVEN a SourceSnapshot v1 or WorkResult v1 payload carrying `kind: "source-snapshot/v1"` or `kind: "work-result/v1"` +- WHEN validated against `source-snapshot/v1.schema.json` or `work-result/v1.schema.json` +- THEN validation MUST succeed +- AND MUST NOT fail with `additionalProperties: false` + +--- + +### Requirement:### Requirement: Canonical V2 Identity Schema Publication And Registry {#REQ-kernel-contract-schemas-013} + +Candidate v2 and WorkOrder v2 schemas MUST be published at filesystem paths `schemas/kernel/candidate/v2.schema.json` and `schemas/kernel/work-order/v2.schema.json` with `$id` values `ospec://schemas/kernel/candidate/v2` and `ospec://schemas/kernel/work-order/v2` respectively. Both schemas MUST be registered in `schemas/kernel/manifest.json` and `schemas/kernel/contract-claims.json`. Publication under wrong directory layouts `schemas/kernel/candidate-v2/` or `schemas/kernel/work-order-v2/` MUST NOT remain as the canonical publication; those paths MUST be removed or replaced by the canonical paths above. + +#### Scenario: V2 schemas resolve at canonical paths and ids + +- GIVEN the published contract suite after this change +- WHEN Candidate v2 and WorkOrder v2 schemas are resolved +- THEN files MUST exist at `schemas/kernel/candidate/v2.schema.json` and `schemas/kernel/work-order/v2.schema.json` +- AND `$id` MUST be `ospec://schemas/kernel/candidate/v2` and `ospec://schemas/kernel/work-order/v2` + +#### Scenario: Manifest and contract-claims register v2 families + +- GIVEN `schemas/kernel/manifest.json` and `schemas/kernel/contract-claims.json` +- WHEN Candidate v2 and WorkOrder v2 entries are inspected +- THEN each family MUST be registered with its canonical path and `$id` +- AND consumers MUST be able to pin those versions via the registry + +#### Scenario: Wrong candidate-v2 and work-order-v2 layouts are not canonical + +- GIVEN residual directories `schemas/kernel/candidate-v2/` or `schemas/kernel/work-order-v2/` +- WHEN publication layout is validated +- THEN those paths MUST NOT be treated as the authoritative v2 schema locations + +--- + +--- + +### Requirement:### Requirement: K1 Historical V1 Content And Pin Restore {#REQ-kernel-contract-schemas-014} + +Historical pre-K3 `candidate/v1` and `work-order/v1` schema file contents and their `K1_SCHEMA_BASELINE` pins MUST be restored from the `02e97a5` era. The system MUST restore file content and update pins to match those restored files. The system MUST NOT retarget `K1_SCHEMA_BASELINE` pins alone to match mutated post-`02e97a5` files while leaving drifted v1 content in place. Verification MUST NOT claim K1 pins intact when v1 schema file digests have drifted from the restored baseline. + +#### Scenario: V1 files and pins match 02e97a5-era baseline + +- GIVEN `schemas/kernel/candidate/v1.schema.json`, `schemas/kernel/work-order/v1.schema.json`, and `K1_SCHEMA_BASELINE` +- WHEN compared to the `02e97a5`-era historical content and pins +- THEN file contents and pin digests MUST match that era +- AND pins MUST hash the restored files + +#### Scenario: Pin-only retarget without content restore is forbidden + +- GIVEN drifted v1 schema files that no longer match `02e97a5`-era content +- WHEN a remediation only rewrites `K1_SCHEMA_BASELINE` pin digests to the drifted files +- THEN that remediation MUST be rejected as non-compliant +- AND verify MUST NOT report pins intact under that condition + +--- + +### Requirement:### Requirement: Execution Graph And Obligation Manifest Schema Family {#REQ-kernel-contract-schemas-015} + +The contract suite MUST publish `execution-graph/v1.schema.json` (`$id: "ospec://schemas/kernel/execution-graph/v1"`) with explicit `schema_version: 1`. The schema MUST require `schema_version`, `graph_id`, `contract_digest`, `policy_bundle_digest`, `policy_snapshot_id`, `source_snapshot_id`, `nodes` (array of semantic graph node objects), and `obligations` (array of obligation items). Both `source_snapshot_id` and `policy_snapshot_id` properties MUST match `^sha256:[a-f0-9]{64}$`. + +In `$defs/node`, the schema MUST define an optional `clarification_context` object property with required fields `event_id` (string), `question_id` (string), and `answer` (string or object), with `additionalProperties: false`. + +Each obligation item MUST require `id`, `criticality` (`must | should | may`), `implemented_by` (array of node IDs), and `required_evidence` (array of evidence identifiers), and MAY include an optional `deferred` object (`reason`, `approved_by`). The schema MUST enforce `additionalProperties: false`. The family MUST ship valid and invalid fixtures demonstrating acceptance of complete graphs with bound source snapshot, policy snapshot provenance, and optional clarification context on nodes, and rejection of missing required fields, malformed source snapshot id, malformed policy snapshot id, or microscopic nodes. +(Previously: execution-graph/v1.schema.json did not define clarification_context on node definitions, causing valid clarify-mutated graphs to fail schema validation.) + +#### Scenario: Valid execution graph with embedded obligations and source snapshot provenance passes validation + +- GIVEN a valid execution graph payload containing semantic nodes, source snapshot provenance, policy snapshot provenance, and an obligation manifest +- WHEN validated against `execution-graph/v1.schema.json` +- THEN validation MUST succeed + +#### Scenario: Execution graph node with clarification_context validates successfully + +- GIVEN an execution graph containing a node mutated with `clarification_context` containing `event_id`, `question_id`, and `answer` +- WHEN validated against `execution-graph/v1.schema.json` +- THEN schema validation MUST succeed + +#### Scenario: Node clarification_context with missing required fields or additional properties fails validation + +- GIVEN an execution graph node with `clarification_context` missing `question_id` or containing unknown additional properties +- WHEN validated against `execution-graph/v1.schema.json` +- THEN validation MUST fail closed identifying the invalid property in `clarification_context` + +#### Scenario: Execution graph missing required fields, policy snapshot, source snapshot provenance, or embedded obligations fails validation + +- GIVEN an execution graph payload missing `policy_snapshot_id`, `source_snapshot_id`, `policy_bundle_digest`, or `obligations` +- WHEN validated against `execution-graph/v1.schema.json` +- THEN validation MUST fail closed identifying the missing property + +#### Scenario: Execution graph with malformed source snapshot id or policy snapshot id fails validation fail-closed + +- GIVEN an execution graph payload containing a `source_snapshot_id` or `policy_snapshot_id` with uppercase characters, wrong length, or invalid prefix +- WHEN validated against `execution-graph/v1.schema.json` +- THEN validation MUST fail closed identifying the malformed property + +--- + +--- + +### Requirement:### Requirement: PolicySnapshot Schema Family With Effective Rules {#REQ-kernel-contract-schemas-016} + +The contract suite MUST publish `policy-snapshot/v1.schema.json` (`$id: "ospec://schemas/kernel/policy-snapshot/v1"`) with explicit `schema_version: 1`. The schema MUST require `snapshot_id`, `policy_bundle_digest`, `compiler_version`, `classifier_version`, `runtime_version`, and `effective_rules` (array of resolved rule strings or objects). The schema MUST enforce `additionalProperties: false` and MUST NOT alias CandidateEvaluationAttestation or DeliveryAuthorization schemas. The family MUST ship valid and invalid fixtures demonstrating valid structure and rejection of malformed or missing fields. + +#### Scenario: Valid PolicySnapshot schema validates successfully + +- GIVEN a valid PolicySnapshot object with complete versions and effective rules +- WHEN validated against `policy-snapshot/v1.schema.json` +- THEN validation MUST succeed + +#### Scenario: PolicySnapshot missing required versions or rules fails validation + +- GIVEN a PolicySnapshot object missing `runtime_version` or `effective_rules` +- WHEN validated against `policy-snapshot/v1.schema.json` +- THEN validation MUST fail closed + +--- + +--- + +### Requirement:### Requirement: ClarifyEvent Schema Family {#REQ-kernel-contract-schemas-017} + +The contract suite MUST publish `clarify-event/v1.schema.json` (`$id: "ospec://schemas/kernel/clarify-event/v1"`) with explicit `schema_version: 1`. The schema MUST require `event_id`, `question_id`, `answer`, `timestamp`, and `affected_nodes` (array of string node IDs). The schema MUST enforce `additionalProperties: false` and strict non-aliasing against general event or transport schemas. The family MUST include valid fixtures and invalid fixtures demonstrating rejection of missing required fields. + +#### Scenario: Valid ClarifyEvent fixture validates successfully + +- GIVEN a valid ClarifyEvent payload declaring question_id, answer, and affected_nodes +- WHEN validated against `clarify-event/v1.schema.json` +- THEN validation MUST succeed + +#### Scenario: ClarifyEvent missing question_id or affected_nodes fails validation + +- GIVEN a ClarifyEvent payload missing `question_id` or `affected_nodes` +- WHEN validated against `clarify-event/v1.schema.json` +- THEN validation MUST fail closed + +--- + +--- + +### Requirement:### Requirement: PolicySnapshot v1 Canonical Binding Validation {#REQ-kernel-contract-schemas-018} + +The contract suite MUST provide a canonical validation function `validatePolicySnapshotBinding(snapshot)` that validates `PolicySnapshot` records against `policy-snapshot/v1.schema.json` and cryptographically verifies that `snapshot.snapshot_id === computePolicySnapshotDigest(snapshot)`. + +The validation function MUST return `{ ok: true }` when: +1. `snapshot` is a non-null object and successfully validates against `ospec://schemas/kernel/policy-snapshot/v1`. +2. `snapshot.snapshot_id` is a valid SHA-256 digest string matching `^sha256:[a-f0-9]{64}$`. +3. The recomputed digest `computePolicySnapshotDigest(snapshot)` exactly equals declared `snapshot.snapshot_id` byte-for-byte. + +If `snapshot` is null, non-object, fails schema validation, has a malformed digest string, or fails cryptographic digest equality, `validatePolicySnapshotBinding` MUST return `{ ok: false, reason_code: "...", error: "..." }` fail-closed (using reason codes `INVALID_PAYLOAD`, `INVALID_SCHEMA`, `ILL_FORMED_SNAPSHOT_ID`, or `POLICY_SNAPSHOT_MISMATCH`). The validator MUST operate as a pure function and MUST NOT mutate the input object. + +#### Scenario: Schema-valid PolicySnapshot with matching cryptographic digest passes validation + +- GIVEN a valid PolicySnapshot object created with canonical versioning and effective rules +- WHEN `validatePolicySnapshotBinding(snapshot)` is executed +- THEN validation MUST return `{ ok: true }` + +#### Scenario: PolicySnapshot with spoofed snapshot_id fails validation with digest mismatch + +- GIVEN a PolicySnapshot object whose declared `snapshot_id` does not match the recomputed `computePolicySnapshotDigest(snapshot)` +- WHEN `validatePolicySnapshotBinding(snapshot)` is executed +- THEN validation MUST return `{ ok: false, reason_code: "POLICY_SNAPSHOT_MISMATCH" }` + +#### Scenario: PolicySnapshot failing JSON schema validation is rejected fail-closed + +- GIVEN a PolicySnapshot object missing required `compiler_version`, `runtime_version`, or `effective_rules` +- WHEN `validatePolicySnapshotBinding(snapshot)` is executed +- THEN validation MUST return `{ ok: false, reason_code: "INVALID_SCHEMA" }` + +#### Scenario: Non-object or malformed PolicySnapshot input fails validation + +- GIVEN a `null`, non-object, or empty input passed to `validatePolicySnapshotBinding` +- WHEN `validatePolicySnapshotBinding(snapshot)` is executed +- THEN validation MUST return `{ ok: false, reason_code: "INVALID_PAYLOAD" }` + +--- + +--- + +### Requirement:### Requirement: Execution Budget And Authority Effect Budget Schema Families {#REQ-kernel-contract-schemas-019} + +The contract suite MUST publish `execution-budget/v1.schema.json` (`$id: "ospec://schemas/kernel/execution-budget/v1"`) and `authority-effect-budget/v1.schema.json` (`$id: "ospec://schemas/kernel/authority-effect-budget/v1"`) with explicit `schema_version: 1`. + +`execution-budget/v1` MUST require `schema_version`, `turns` (integer > 0), `patches` (integer >= 0), `commands` (integer >= 0), `wall_time_minutes` (number > 0), `changed_lines` (integer > 0), and `allowed_paths` (array of strings). + +`authority-effect-budget/v1` MUST require `schema_version`, `effect_attempts` (integer > 0), `authority_mutations` (integer >= 0), `evidence_runs` (integer >= 0), and `review_sweeps` (integer >= 0). + +Both schemas MUST enforce `additionalProperties: false`. The family MUST ship valid and invalid fixtures demonstrating rejection of negative quotas, missing required fields, or unallowlisted properties. + +#### Scenario: Valid execution budget and authority budget fixtures pass validation + +- GIVEN valid execution-budget and authority-effect-budget payload objects +- WHEN validated against `execution-budget/v1.schema.json` and `authority-effect-budget/v1.schema.json` +- THEN validation MUST succeed + +#### Scenario: Budget fixture with negative quota or missing field fails validation + +- GIVEN an execution budget fixture with `turns: -1` or missing `changed_lines` +- WHEN schema validation runs +- THEN validation MUST fail closed identifying the invalid property + +--- + +--- + +### Requirement:### Requirement: Causal Failure And Recovery Transition Schema Families {#REQ-kernel-contract-schemas-020} + +The contract suite MUST publish `causal-failure/v1.schema.json` (`$id: "ospec://schemas/kernel/causal-failure/v1"`) and `failure-recovery-transition/v1.schema.json` (`$id: "ospec://schemas/kernel/failure-recovery-transition/v1"`) with explicit `schema_version: 1`. + +`causal-failure/v1` MUST require `schema_version`, `failure_id` (string), `category` (`environment_tooling | cas_conflict | ambiguous_effect | validation_gap | code_defect`), `code` (string), `priority` (integer 1-5), `blocking_fingerprint` (string), and `details` (object). + +`failure-recovery-transition/v1` MUST require `schema_version`, `transition_id` (string), `failure_code` (string), `target_operation` (`repair | replan | escalate | stop`), `scope` (object with `node_ids`, `allowed_paths`, `finding_ids`), and `expected_advancement` (boolean). + +Both schemas MUST enforce `additionalProperties: false` and ship valid and invalid fixtures. + +#### Scenario: Valid causal failure and recovery transition fixtures pass validation + +- GIVEN valid causal-failure and failure-recovery-transition payloads +- WHEN validated against their respective schemas +- THEN validation MUST succeed + +#### Scenario: Causal failure with invalid category fails validation + +- GIVEN a causal-failure payload with category `unknown_error` +- WHEN validated against `causal-failure/v1.schema.json` +- THEN validation MUST fail closed identifying the invalid category enum + +--- + +--- + +### Requirement:### Requirement: Workspace Descriptor And Capsule Definition Schema Families {#REQ-kernel-contract-schemas-021} + +The contract suite MUST publish `workspace-descriptor/v1.schema.json` (`$id: "ospec://schemas/kernel/workspace-descriptor/v1"`) and `capsule-definition/v1.schema.json` (`$id: "ospec://schemas/kernel/capsule-definition/v1"`) with explicit `schema_version: 1`. + +`workspace-descriptor/v1` MUST require `schema_version`, `workspace_id` (string matching `^ws-[a-f0-9-]+$`), `root_path` (string), `source_snapshot_id` (string matching `^sha256:[a-f0-9]{64}$`), `status` (`active | disposed | interrupted`), and `created_at` (ISO date-time string). + +`capsule-definition/v1` MUST require `schema_version`, `capsule_id` (string), `fingerprint` (string matching `^sha256:[a-f0-9]{64}$`), `source_snapshot_id` (string matching `^sha256:[a-f0-9]{64}$`), `dependencies` (array of SHA-256 WorkOrderId strings matching `^sha256:[a-f0-9]{64}$` or dependency strings), `allowed_paths` (array of string path patterns), and `environment` (object), and MAY declare `capsule_inputs` (array of relative file path strings). + +Both schemas MUST enforce `additionalProperties: false` and ship valid and invalid fixtures demonstrating schema validation and rejection of missing required fields or invalid property patterns. +(Previously: Capsule definition schema did not support decoupled capsule_inputs alongside SHA-256 DAG dependencies.) + +#### Scenario: Valid workspace descriptor and capsule definition fixtures pass validation + +- GIVEN valid workspace-descriptor and capsule-definition payload objects +- WHEN validated against their respective schemas +- THEN validation MUST succeed + +#### Scenario: Workspace descriptor with invalid status or malformed source_snapshot_id fails validation + +- GIVEN a workspace descriptor fixture with `status: "unknown"` or malformed `source_snapshot_id` +- WHEN validated against `workspace-descriptor/v1.schema.json` +- THEN validation MUST fail closed identifying the invalid property + +#### Scenario: Capsule definition missing allowed_paths or dependencies fails validation + +- GIVEN a capsule definition payload missing `allowed_paths` or `dependencies` +- WHEN validated against `capsule-definition/v1.schema.json` +- THEN validation MUST fail closed identifying the missing required property + +--- + +--- + +### Requirement:### Requirement: Work Result Execution Payload And Containment Violation Schema Families {#REQ-kernel-contract-schemas-022} + +The contract suite MUST publish `work-result-execution-payload/v1.schema.json` (`$id: "ospec://schemas/kernel/work-result-execution-payload/v1"`) and `containment-violation/v1.schema.json` (`$id: "ospec://schemas/kernel/containment-violation/v1"`) with explicit `schema_version: 1`. + +`work-result-execution-payload/v1` MUST require `schema_version`, `work_result_id` (string matching `^sha256:[a-f0-9]{64}$`), `work_order_id` (string matching `^sha256:[a-f0-9]{64}$`), `source_snapshot_id` (string matching `^sha256:[a-f0-9]{64}$`), `patch` (string), `commands` (array of command outcome objects), `logs` (array of strings), `exit_code` (integer), `filesystem_inventory` (array of file objects), and `execution_usage` (object conforming to execution usage schema). The schema MUST strictly prohibit any `candidate_id` property. + +`containment-violation/v1` MUST require `schema_version`, `violation_id` (string), `workspace_id` (string), `work_order_id` (string), `attempted_path` (string), `allowed_paths` (array of strings), `violation_type` (`traversal | symlink_escape | undeclared_write | permission_denied`), and `timestamp` (ISO date-time string). + +Both schemas MUST enforce `additionalProperties: false`. Negative non-aliasing fixtures MUST demonstrate that `work-result-execution-payload` cannot validate as `Candidate` or `DeliveryAuthorization`, and `containment-violation` cannot validate as `OperationReceipt` or `transport-failure`. + +#### Scenario: Valid containment violation fixture passes validation + +- GIVEN a valid containment violation payload declaring attempted_path and violation_type `traversal` +- WHEN validated against `containment-violation/v1.schema.json` +- THEN validation MUST succeed + +#### Scenario: Containment violation with unknown violation_type fails validation + +- GIVEN a containment violation payload with `violation_type: "kernel_panic"` +- WHEN validated against `containment-violation/v1.schema.json` +- THEN validation MUST fail closed identifying the invalid enum value + +#### Scenario: Valid work result execution payload passes validation + +- GIVEN a valid work-result-execution-payload object with valid cryptographic digests and execution usage +- WHEN validated against `work-result-execution-payload/v1.schema.json` +- THEN validation MUST succeed + +#### Scenario: WorkResult payload declaring candidate_id fails validation + +- GIVEN a work result execution payload containing property `candidate_id` +- WHEN validated against `work-result-execution-payload/v1.schema.json` +- THEN validation MUST fail closed due to forbidden candidate property + +--- + +### Requirement:### Requirement: Work Order V2 Requires Closed Capsule Inputs {#REQ-kernel-contract-schemas-023} + +`work-order/v2.schema.json` MUST add `capsule_inputs` as a required property: an array of one or more non-empty relative file-path strings. Each item MUST be a concrete relative path (`minLength: 1`), MUST NOT match glob metacharacters (`*`, `?`, `[`), MUST NOT contain `..`, and MUST NOT be absolute. `additionalProperties` MUST remain `false`. `work-order/v1.schema.json` and `K1_SCHEMA_BASELINE` MUST remain byte-identical. + +Valid v2 fixtures MUST include `capsule_inputs`. Negative fixtures MUST reject: omitted `capsule_inputs`, empty array, non-array, glob items, `..` traversal, and absolute paths. Capsule-definition `capsule_inputs` (MAY on `capsule-definition/v1`) is unchanged and MUST NOT be treated as a substitute for WorkOrder v2 `capsule_inputs`. + +#### Scenario: WorkOrder v2 with valid capsule_inputs passes validation + +- GIVEN a WorkOrder v2 payload that includes `capsule_inputs: ["src/app.js"]` and all other required v2 fields +- WHEN validated against `work-order/v2.schema.json` +- THEN validation MUST succeed + +#### Scenario: WorkOrder v2 missing or empty capsule_inputs fails closed + +- GIVEN a WorkOrder v2 payload with omitted `capsule_inputs` or `capsule_inputs: []` +- WHEN validated against `work-order/v2.schema.json` +- THEN validation MUST fail closed identifying `capsule_inputs` + +#### Scenario: Glob, traversal, or absolute capsule_inputs items fail closed + +- GIVEN a WorkOrder v2 payload whose `capsule_inputs` contains `src/**`, `../secret`, or `/abs/path` +- WHEN validated against `work-order/v2.schema.json` +- THEN validation MUST fail closed identifying the invalid item + +#### Scenario: WorkOrder v1 and K1 pins remain frozen + +- GIVEN `work-order/v1.schema.json` and `K1_SCHEMA_BASELINE` +- WHEN verified after the v2 `capsule_inputs` addition +- THEN v1 schema bytes and K1 pins MUST remain byte-identical to the frozen baseline + +### Requirement:### Requirement: Evidence V2 Provenance Binding Distinct From Verdict {#REQ-kernel-contract-schemas-024} + +The suite MUST publish `evidence/v2.schema.json` +(`$id: "ospec://schemas/kernel/evidence/v2"`) with `schema_version: 2` and +`kind: "evidence/v2"`. Required fields: `schema_version`, `kind`, +`evidence_id`, `candidate_id` (`^sha256:[a-f0-9]{64}$`), `provenance` +(`runtime-observed | host-attested | tool-produced | model-reported | +human-decision | external-unverified`), `origin`, `digest` +(`^sha256:[a-f0-9]{64}$`), and `node_id`. The schema MUST forbid `verdict` +and MUST enforce `additionalProperties: false`. `evidence/v1.schema.json`, +its K1 fixtures, and `K1_SCHEMA_BASELINE` pins MUST remain byte-identical. +Valid and invalid fixtures MUST cover complete provenance-bound evidence, +missing required fields, unknown provenance, malformed `candidate_id`, and +payloads that include `verdict`. + +#### Scenario: Valid evidence v2 fixture passes + +- GIVEN a complete evidence/v2 payload with `runtime-observed` provenance and a valid CandidateId +- WHEN validated against `evidence/v2.schema.json` +- THEN validation MUST succeed + +#### Scenario: Evidence v2 with verdict or unknown provenance fails closed + +- GIVEN an evidence/v2 payload that includes `verdict` or `provenance: "worker-said-so"` +- WHEN schema validation runs +- THEN validation MUST fail closed identifying the violating property + +#### Scenario: Evidence v1 pins remain frozen + +- GIVEN `evidence/v1.schema.json`, its K1 fixtures, and `K1_SCHEMA_BASELINE` +- WHEN verified after evidence/v2 publication +- THEN v1 schema and fixture bytes and K1 pins MUST remain byte-identical + +### Requirement:### Requirement: Verification V2 Verdict Distinct From Evidence {#REQ-kernel-contract-schemas-025} + +The suite MUST publish `verification/v2.schema.json` +(`$id: "ospec://schemas/kernel/verification/v2"`) with `schema_version: 2` and +`kind: "verification/v2"`. Required fields: `schema_version`, `kind`, +`verification_id`, `candidate_id` (`^sha256:[a-f0-9]{64}$`), `verdict` +(`PASS | PASS WITH WARNINGS | FAIL`), and `evidence_ids` (array of +`^sha256:[a-f0-9]{64}$`). The schema MUST enforce `additionalProperties: false`. +`verification/v1.schema.json`, its K1 fixtures, and `K1_SCHEMA_BASELINE` pins +MUST remain byte-identical. Negative non-aliasing fixtures MUST reject +verification/v2 as evidence/v2 and evidence/v2 as verification/v2. + +#### Scenario: Valid verification v2 fixture passes + +- GIVEN a verification/v2 payload with `verdict: "PASS"` and bound evidence_ids +- WHEN validated against `verification/v2.schema.json` +- THEN validation MUST succeed + +#### Scenario: Cross-family substitution fails closed + +- GIVEN an evidence/v2 payload validated as verification/v2, or a verification/v2 payload validated as evidence/v2 +- WHEN schema validation runs +- THEN validation MUST fail closed identifying kind or required-field mismatch + +#### Scenario: Verification v1 pins remain frozen + +- GIVEN `verification/v1.schema.json`, its K1 fixtures, and `K1_SCHEMA_BASELINE` +- WHEN verified after verification/v2 publication +- THEN v1 schema and fixture bytes and K1 pins MUST remain byte-identical + +### Requirement:### Requirement: Assurance Graph Schema Family And Equivalence Manifest {#REQ-kernel-contract-schemas-026} + +The suite MUST publish `assurance-graph/v1.schema.json` +(`$id: "ospec://schemas/kernel/assurance-graph/v1"`) with `schema_version: 1`. +Required fields: `schema_version`, `graph_id` (`^sha256:[a-f0-9]{64}$`), +`candidate_id` (`^sha256:[a-f0-9]{64}$`), `nodes` (array), and `edges` (array +of `{from, relation, to}` with `relation` in +`verified-by | satisfies | derived-from | invalidates`). The schema MUST +enforce `additionalProperties: false` and MUST NOT alias +CandidateEvaluationAttestation or DeliveryAuthorization. An optional +equivalence-manifest object MAY appear with a distinct `kind` and MUST NOT +validate as attestation or authorization. The family MUST ship valid fixtures +and invalid fixtures for missing fields, unknown relation, and malformed +digests. + +#### Scenario: Valid assurance-graph fixture passes + +- GIVEN a complete assurance-graph/v1 payload with four-relation edges and matching graph_id digest form +- WHEN validated against `assurance-graph/v1.schema.json` +- THEN validation MUST succeed + +#### Scenario: Unknown relation or attestation alias fails closed + +- GIVEN an edge with `relation: "reviewed-by"` or a graph payload that validates as CandidateEvaluationAttestation +- WHEN schema validation runs +- THEN validation MUST fail closed + +### Requirement: Additive Assessment Binding Family Distinct From Evidence And Verification {#REQ-kernel-contract-schemas-027} + +The suite MUST publish an additive assessment/binding schema family with a +distinct `$id` and explicit `schema_version`. Exact `$id` is design-owned. +Required persistable fields: assessment identity, `evidence_id`, `role`, +`obligation_id`, `node_id`, `candidate_id` (`^sha256:[a-f0-9]{64}$`), bound +policy-snapshot identity, and an additive coverage field recording the +satisfied `required_evidence` tokens (exact field name design-owned). +Assessment identity MUST incorporate `role` and `obligation_id`. The schema +MUST enforce `additionalProperties: false` and MUST NOT include `verdict`. +The family MUST NOT validate as `evidence/v2` or `verification/v2`. +`evidence/v2`, `verification/v2`, and K1 v1 schema bytes and +`K1_SCHEMA_BASELINE` pins MUST remain byte-identical. Valid and invalid +fixtures MUST cover a complete binding including coverage, missing required +fields, omitted coverage, and cross-family substitution. +(Previously: assessment/binding required identity fields but no persistable coverage of satisfied required_evidence tokens.) + +#### Scenario: Valid assessment fixture passes + +- GIVEN a complete assessment/binding payload with role, obligation_id, node_id, evidence_id, policy-snapshot identity, and the additive coverage field +- WHEN validated against the assessment/binding schema +- THEN validation MUST succeed + +#### Scenario: Cross-family substitution and verdict fail closed + +- GIVEN an assessment payload validated as evidence/v2 or verification/v2, or an assessment payload that includes `verdict` +- WHEN schema validation runs +- THEN validation MUST fail closed identifying kind or required-field mismatch + +#### Scenario: Four-role assessments remain distinct under the schema + +- GIVEN four assessment payloads that share one `evidence_id` and differ only by `role` +- WHEN each is validated and identities are compared +- THEN all four MUST be schema-valid +- AND their assessment identities MUST be pairwise distinct + +#### Scenario: Assessment fixture without coverage field fails closed + +- GIVEN an assessment/binding payload that omits the additive coverage field +- WHEN validated against the assessment/binding schema +- THEN validation MUST fail closed identifying the missing coverage field + +#### Scenario: Evidence v2, verification v2, and K1 v1 pins remain frozen + +- GIVEN `evidence/v2.schema.json`, `verification/v2.schema.json`, K1 v1 schemas, and `K1_SCHEMA_BASELINE` +- WHEN verified after the additive coverage field is present on assessment/binding +- THEN those schema and fixture bytes and K1 pins MUST remain byte-identical diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/proposal.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/proposal.md new file mode 100644 index 00000000..5f4de54a --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/proposal.md @@ -0,0 +1,73 @@ +# Proposal: k6b-semantic-integrity-remediation + +## Intent + +Cerrar seis defectos semánticos post-v2.51.0 (B1–B3, H1–H3) sin reabrir el macro-slice K6b ni iniciar K6c. Un Set de roles no equivale a evidencia no aliasable; `required_evidence.length > 0` no equivale a cobertura token a token; projector, replay y reconcile no fallan cerrados ante inputs contradictorios o payloads tampered. + +## Scope + +### In Scope +- **B1 (corrección de REQ-006):** roles incompatibles MUST NOT compartir un EvidenceId; Strict TDD es RED → GREEN (no un Set); GREEN-before-RED y RED-after-PATCH fallan. `assessment_id` distintos por tupla `(evidence, role, obligation)`; la misma observación no satisface roles incompatibles. +- **B2:** MUST-walk exige `required_evidence ⊆ evidence_requirements_satisfied` (como K4a). Persistir cobertura en assessment/binding (campo aditivo; nombre design-owned). +- **B3:** Antes de strategy, `input.contract.contract_digest === executionGraph.contract_digest`; divergencia fail-closed. +- **H1:** `projectAssuranceGraph` fail-closed si canonicalInputs contradicen Graph/contract/policy o si la preimage admite digests null. +- **H2:** Replay revalida schema y recomputa `assessment_id`/`candidate_id`/`policy_snapshot_id`; evidencia, obligation, nodo implementador y `node_id`. +- **H3:** Reconcile también nodes, canonical_inputs, candidate_id y kind/schema (o recompute `graph_id` del stored). +- Tests adversariales. Docs: K6b `revise`; K6c `blocked-by-K6b-remediation` hasta archive. + +### Out of Scope +- Macro-slice K6b; K6c/K6d/K7/K8; PKI; mutar `evidence/v2` o K1 v1; rediseñar K6b; provenance v2.51.0. + +## Capabilities + +### New Capabilities +- None. + +### Modified Capabilities +- `independent-verification`: REQ-006 deja de exigir relabeling de cuatro roles sobre el mismo EvidenceId; cobertura token a token; gate de digests contract ↔ Execution Graph antes de strategy. +- `assurance-graph`: projector, replay y reconcile fail-closed (H1–H3). +- `kernel-contract-schemas`: campo aditivo de cobertura en assessment/binding; pins `evidence/v2`, `verification/v2` y K1 v1 intactos. + +## Approach + +Corregir spec y runtime juntos. Strategy exige EvidenceIds distintos para roles incompatibles y orden temporal Strict TDD. MUST-walk consume cada token y persiste refs aditivas. Gate previo a strategy iguala contract digests. El projector público rechaza canonicalInputs contradictorios y preimages con digest null. Replay revalida assessments; reconcile compara el stored payload (o su `graph_id` recompute). Schema solo aditivo. + +## Affected Areas + +| Area | Impact | Description | +|------|--------|-------------| +| `scripts/lib/independent-verifier/` | Modified | B1–B3 + invertir REQ-006 | +| `scripts/lib/assurance-graph/` | Modified | H1–H3 | +| `schemas/kernel/assessment/` | Modified (additive) | Coverage field + fixtures | +| `openspec/specs/{independent-verification,assurance-graph,kernel-contract-schemas}/` | Modified | Deltas | +| `docs/{architecture,roadmaps}/harness-evolution.md` | Modified | K6b revise; K6c blocked | + +## Risks + +| Risk | Likelihood | Mitigation | +|------|------------|------------| +| Tests/spec v2.51.0 exigen aliasing de cuatro roles | High | Corrección conjunta spec+test+runtime | +| Campo aditivo leído como mutación de `evidence/v2` | Med | Familia assessment/binding; pins byte-identical | +| Docs dejan K6c `next-eligible` | Med | Roadmap en este change | + +## Rollback Plan + +Revertir runtime, schema aditivo, tests y docs como unidad. Conservar `evidence/v2`, `verification/v2` y pins K1. No migrar assessments parciales. K6b queda `revise` y K6c bloqueado hasta archive conforme. + +## Dependencies + +- Integridad K6b v2.51.0 (`2026-08-27-k6b-verification-integrity-remediation`); Obligation Manifest K4a; schemas `assessment/v1`, `evidence/v2`, `assurance-graph/v1`. Delivery: `exception-ok`. + +## Success Criteria + +- [ ] Mismo EvidenceId como RED+GREEN ⇒ FAIL. +- [ ] GREEN before RED ⇒ FAIL. +- [ ] RED after PATCH ⇒ FAIL. +- [ ] `required_evidence = [A,B]` y solo A ⇒ FAIL. +- [ ] Graph.contract=C1 y input.contract=C2 ⇒ FAIL. +- [ ] Graph=C1 y canonicalInputs=C2 ⇒ `GRAPH_DIVERGENCE`. +- [ ] `assessment_id` tampered ⇒ `GRAPH_DIVERGENCE` / invalid assessment. +- [ ] Stored nodes tampered ⇒ `GRAPH_DIVERGENCE`. +- [ ] Roadmap: K6b `revise`; K6c `blocked-by-K6b-remediation` hasta archive. + +> **Branch advisory:** Before `sdd-apply` begins, a feature branch SHOULD be created following the `/` convention defined in the `branch-pr` skill (e.g. `git checkout -b feat/my-change main`). This note is SHOULD, not MUST — omit it from `status: blocked` envelopes. diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md new file mode 100644 index 00000000..fba278d0 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md @@ -0,0 +1,95 @@ +# Delta for Assurance Graph + +## ADDED Requirements + +### Requirement: Projector Fail-Closed On Contradictory Canonical Inputs {#REQ-assurance-graph-007} + +`projectAssuranceGraph` MUST fail closed with `GRAPH_DIVERGENCE` when supplied +`canonicalInputs` contradict the Graph, contract, or policy they claim to +project. Required canonical input digests MUST be present and non-null. The +projector MUST NOT fingerprint a null or absent digest for any required +canonical input in the `graph_id` preimage. Contradiction or a null required +digest MUST NOT yield a successful projection. + +#### Scenario: Graph contract contradicts canonicalInputs + +- GIVEN an Execution Graph whose contract digest is C1 +- AND `canonicalInputs` whose contract digest is C2, where C1 ≠ C2 +- WHEN `projectAssuranceGraph` runs +- THEN projection MUST fail closed with `GRAPH_DIVERGENCE` +- AND MUST NOT emit a graph whose `graph_id` fingerprints the contradictory pair + +#### Scenario: Null required canonical digest is not fingerprinted + +- GIVEN a required canonical input whose digest is null or absent +- WHEN `projectAssuranceGraph` runs +- THEN projection MUST fail closed +- AND MUST NOT produce a `graph_id` that fingerprints a null digest for that input + +### Requirement: Reconcile Stored Payload Divergence {#REQ-assurance-graph-008} + +`reconcileAssuranceGraph` MUST detect stored-graph divergence beyond equality +of `graph_id` and edges. It MUST fail closed with `GRAPH_DIVERGENCE` when +stored `nodes`, `canonical_inputs`, `candidate_id`, or kind/schema diverge +from the recomputed projection, or when `graph_id` recomputed from the stored +payload does not match the stored `graph_id`. Comparing only stored +`graph_id` plus edges MUST NOT be sufficient to declare reconciliation OK. + +#### Scenario: Tampered stored nodes fail closed + +- GIVEN a stored Assurance Graph whose `graph_id` and edges match a recomputation +- AND whose persistable `nodes` have been altered +- WHEN `reconcileAssuranceGraph` runs +- THEN validation MUST fail closed with `GRAPH_DIVERGENCE` + +#### Scenario: Stored payload or identity fields diverge + +- GIVEN a stored Assurance Graph whose `candidate_id`, persistable `canonical_inputs`, or kind/schema differ from the current projection, or whose declared `graph_id` does not equal the digest recomputed from its stored payload +- WHEN `reconcileAssuranceGraph` runs +- THEN validation MUST fail closed with `GRAPH_DIVERGENCE` + +## MODIFIED Requirements + +### Requirement: Replay From Persistable Outputs {#REQ-assurance-graph-006} + +The Assurance Graph MUST be reproducible from persistable outputs: canonical +input digests, projected nodes and edges, evidence records, verification +records, and assessment/binding records that carry `obligation_id`. Replay +MUST NOT require ephemeral projector fields. Consumers MUST NOT reinvent +`obligation_id` values from vanished fields. `satisfies` edges MUST be +rebuildable from persistable assessments. `replayAssuranceGraph` MUST +revalidate each persisted assessment before accepting the replay: schema +validity; recomputed assessment identity; `candidate_id` match; bound +policy-snapshot identity; referenced evidence exists; `obligation_id` is in +the graph; the bound node implements that obligation; and persistable +`node_id` matches the evidence record. Any check failure MUST fail closed +with `GRAPH_DIVERGENCE` or as an invalid assessment. A tampered +`assessment_id` MUST NOT replay as valid. +(Previously: replay compared recomputed graph_id and edges without revalidating persisted assessments.) + +#### Scenario: Replay from persisted outputs yields the same graph + +- GIVEN a previously projected graph plus persisted evidence, verification, assessments, and canonical input digests +- AND no ephemeral `obligation_ids` on the original projector call +- AND every persisted assessment passes revalidation +- WHEN the graph is replayed from those persistable outputs only +- THEN the recomputed `graph_id` and edge set MUST be byte-identical to the stored graph + +#### Scenario: Tampered assessment_id fails replay + +- GIVEN persisted assessments plus a stored graph that would otherwise replay +- AND one assessment whose `assessment_id` does not match the identity recomputed from its persistable fields +- WHEN `replayAssuranceGraph` runs +- THEN replay MUST fail closed with `GRAPH_DIVERGENCE` or as an invalid assessment + +#### Scenario: Assessment fails schema, candidate, or policy revalidation + +- GIVEN a persisted assessment that fails schema validation, whose `candidate_id` does not match the graph subject, or whose bound policy-snapshot identity does not match the graph +- WHEN `replayAssuranceGraph` runs +- THEN replay MUST fail closed with `GRAPH_DIVERGENCE` or as an invalid assessment + +#### Scenario: Assessment bound to missing evidence or non-implementing node fails replay + +- GIVEN a persisted assessment whose `evidence_id` does not exist, whose `obligation_id` is absent from the graph, whose node does not implement that obligation, or whose `node_id` disagrees with the evidence record +- WHEN `replayAssuranceGraph` runs +- THEN replay MUST fail closed with `GRAPH_DIVERGENCE` or as an invalid assessment diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/specs/independent-verification/spec.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/specs/independent-verification/spec.md new file mode 100644 index 00000000..461f0f99 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/specs/independent-verification/spec.md @@ -0,0 +1,115 @@ +# Delta for Independent Verification + +## ADDED Requirements + +### Requirement: Contract Digest Gate Before Strategy {#REQ-independent-verification-008} + +Before strategy evaluation, `input.contract.contract_digest` MUST equal +`executionGraph.contract_digest`. Mismatch MUST fail closed and MUST NOT +proceed to strategy evaluation. + +#### Scenario: Contract digest mismatch with Execution Graph fails closed before strategy + +- GIVEN a frozen Candidate whose `input.contract.contract_digest` is C2 +- AND an Execution Graph whose `contract_digest` is C1, where C1 ≠ C2 +- WHEN the verifier validates canonical bindings +- THEN verification MUST fail closed before strategy evaluation +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +## MODIFIED Requirements + +### Requirement: Obligation Manifest MUST Coverage {#REQ-independent-verification-005} + +After strategy evaluation, the verifier MUST walk every Obligation Manifest +item with criticality `must` that is not an approved `deferred` record +(`reason` and `approved_by`). For each such obligation, a `PASS` or +`PASS WITH WARNINGS` verdict MUST require `required_evidence` ⊆ persistable +satisfied tokens, each persistably bound to that `obligation_id` and to a +`node_id` listed in `implemented_by`. Coverage MUST be persisted on the +assessment/binding record (additive field; exact name design-owned). A +non-empty evidence list, unique-sort of `evidence_ids`, or existential +binding MUST NOT substitute for that subset. Strategy role shape alone MUST +NOT satisfy the graph. An `obligation_id` absent from the manifest MUST fail +closed. Evidence bound to a node that does not implement the obligation MUST +fail closed. A MUST whose `required_evidence` is not a subset of satisfied +tokens MUST fail closed and MUST identify the unfulfilled `obligation_id`. +The verifier MUST consume persistable manifest `obligation_id` values; it +MUST NOT invent them from vanished fields. +(Previously: coverage required admissible evidence "covering" required_evidence without subset semantics or persistable satisfied tokens.) + +#### Scenario: MUST without admissible evidence fails closed + +- GIVEN a compiled Execution Graph with a non-deferred MUST obligation +- AND strategy role minimums met +- AND no admissible evidence persistably bound to that `obligation_id` +- WHEN the verifier evaluates obligation coverage +- THEN it MUST fail closed identifying that `obligation_id` +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +#### Scenario: Nonexistent obligation_id fails closed + +- GIVEN an assessment or raw binding whose `obligation_id` is not in the Obligation Manifest +- WHEN the verifier evaluates obligation coverage +- THEN it MUST fail closed identifying the unknown `obligation_id` +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +#### Scenario: Evidence bound to the wrong implementing node fails closed + +- GIVEN a MUST obligation with `implemented_by` containing node A +- AND admissible evidence whose persistable `node_id` is B, where B is not in `implemented_by` +- WHEN the verifier evaluates that binding +- THEN it MUST fail closed +- AND MUST NOT treat the evidence as satisfying that obligation + +#### Scenario: Partial required_evidence coverage fails closed + +- GIVEN a non-deferred MUST obligation with `required_evidence` equal to `[A, B]` +- AND admissible evidence that satisfies only token A +- WHEN the verifier evaluates obligation coverage +- THEN it MUST fail closed identifying that `obligation_id` +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +### Requirement: Persistable Assessment Binding Distinct From Evidence {#REQ-independent-verification-006} + +The verifier MUST persist an additive assessment/binding record for each +evaluated tuple of `evidence_id`, `role`, `obligation_id`, `node_id`, and bound +policy-snapshot identity. Assessment identity MUST include `role` and +`obligation_id` and MUST be distinct per `(evidence_id, role, obligation_id)`. +`evidence/v2` MUST remain the observation record and MUST NOT be mutated to +carry `role` or `obligation_id`. Incompatible strategy roles MUST NOT share +one EvidenceId. The same observation MUST NOT satisfy incompatible roles. +Strict TDD evidence is a RED → GREEN sequence, not a set of role labels: +GREEN-before-RED MUST fail closed, and RED-after-PATCH MUST fail closed. +Unique-sort of `verification.evidence_ids` MUST NOT be the assessment identity +and MUST NOT hide distinct role or obligation bindings. +(Previously: the same EvidenceId used as four roles was required to yield four passing assessments.) + +#### Scenario: Same EvidenceId as RED and GREEN fails closed + +- GIVEN one `evidence/v2` observation whose `evidence_id` is E +- AND that observation is bound as both RED and GREEN strategy roles +- WHEN the verifier evaluates strategy evidence +- THEN verification MUST fail closed +- AND MUST NOT treat E as satisfying both roles + +#### Scenario: GREEN before RED fails closed + +- GIVEN Strict TDD or bug-strategy evidence whose GREEN observation precedes RED +- WHEN the verifier evaluates strategy evidence +- THEN verification MUST fail closed +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +#### Scenario: RED after PATCH fails closed + +- GIVEN Strict TDD or bug-strategy evidence whose RED observation occurs after PATCH +- WHEN the verifier evaluates strategy evidence +- THEN verification MUST fail closed +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +#### Scenario: Distinct tuples yield distinct assessment identities + +- GIVEN two persistable tuples that differ in `evidence_id`, `role`, or `obligation_id` +- AND neither tuple shares one EvidenceId across incompatible roles +- WHEN assessments are persisted +- THEN their assessment identities MUST be distinct +- AND unique-sort of `verification.evidence_ids` MUST NOT collapse those assessments diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md new file mode 100644 index 00000000..0efdeccf --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md @@ -0,0 +1,51 @@ +# Delta for kernel-contract-schemas + +## MODIFIED Requirements + +### Requirement: Additive Assessment Binding Family Distinct From Evidence And Verification {#REQ-kernel-contract-schemas-027} + +The suite MUST publish an additive assessment/binding schema family with a +distinct `$id` and explicit `schema_version`. Exact `$id` is design-owned. +Required persistable fields: assessment identity, `evidence_id`, `role`, +`obligation_id`, `node_id`, `candidate_id` (`^sha256:[a-f0-9]{64}$`), bound +policy-snapshot identity, and an additive coverage field recording the +satisfied `required_evidence` tokens (exact field name design-owned). +Assessment identity MUST incorporate `role` and `obligation_id`. The schema +MUST enforce `additionalProperties: false` and MUST NOT include `verdict`. +The family MUST NOT validate as `evidence/v2` or `verification/v2`. +`evidence/v2`, `verification/v2`, and K1 v1 schema bytes and +`K1_SCHEMA_BASELINE` pins MUST remain byte-identical. Valid and invalid +fixtures MUST cover a complete binding including coverage, missing required +fields, omitted coverage, and cross-family substitution. +(Previously: assessment/binding required identity fields but no persistable coverage of satisfied required_evidence tokens.) + +#### Scenario: Valid assessment fixture passes + +- GIVEN a complete assessment/binding payload with role, obligation_id, node_id, evidence_id, policy-snapshot identity, and the additive coverage field +- WHEN validated against the assessment/binding schema +- THEN validation MUST succeed + +#### Scenario: Cross-family substitution and verdict fail closed + +- GIVEN an assessment payload validated as evidence/v2 or verification/v2, or an assessment payload that includes `verdict` +- WHEN schema validation runs +- THEN validation MUST fail closed identifying kind or required-field mismatch + +#### Scenario: Four-role assessments remain distinct under the schema + +- GIVEN four assessment payloads that share one `evidence_id` and differ only by `role` +- WHEN each is validated and identities are compared +- THEN all four MUST be schema-valid +- AND their assessment identities MUST be pairwise distinct + +#### Scenario: Assessment fixture without coverage field fails closed + +- GIVEN an assessment/binding payload that omits the additive coverage field +- WHEN validated against the assessment/binding schema +- THEN validation MUST fail closed identifying the missing coverage field + +#### Scenario: Evidence v2, verification v2, and K1 v1 pins remain frozen + +- GIVEN `evidence/v2.schema.json`, `verification/v2.schema.json`, K1 v1 schemas, and `K1_SCHEMA_BASELINE` +- WHEN verified after the additive coverage field is present on assessment/binding +- THEN those schema and fixture bytes and K1 pins MUST remain byte-identical diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/state.yaml b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/state.yaml new file mode 100644 index 00000000..60d463d4 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/state.yaml @@ -0,0 +1,185 @@ +change: "k6b-semantic-integrity-remediation" +status: "archived" +last_updated: "2026-08-27T22:10:00Z" +classification: high-risk +blocking_questions: [] +phases: + proposal: + status: done + artifact: "openspec/changes/k6b-semantic-integrity-remediation/proposal.md" + summary: "Focal B1–B3/H1–H3: REQ-006 aliasing, required_evidence subset, digest gate, projector/replay/reconcile fail-closed." + key_decisions: + - "REQ-006 is a spec correction: incompatible roles MUST NOT share one EvidenceId" + - "Coverage field is additive on assessment/binding; evidence/v2 and K1 v1 stay frozen" + - "Harden projectAssuranceGraph itself, not only verifyCandidate" + spec: + status: done + contract_remediation: resolved + artifacts: + - "openspec/changes/k6b-semantic-integrity-remediation/specs/independent-verification/spec.md" + - "openspec/changes/k6b-semantic-integrity-remediation/specs/assurance-graph/spec.md" + - "openspec/changes/k6b-semantic-integrity-remediation/specs/kernel-contract-schemas/spec.md" + summary: "Deltas B1–B3/H1–H3: invert REQ-006 aliasing; subset coverage; digest gate; projector/replay/reconcile fail-closed." + key_decisions: + - "REQ-006 inverted: incompatible roles MUST NOT share one EvidenceId; distinct ids per tuple remain" + - "Coverage field additive on assessment/binding; name design-owned; evidence/v2 and K1 v1 frozen" + - "H1–H3 bind projectAssuranceGraph, replayAssuranceGraph, and reconcileAssuranceGraph" + clarify: + status: skipped + rationale: "Validated sdd-spec signals reported residual_ambiguity=false with empty public_contract_questions, conflicting_requirements, and missing_acceptance_criteria arrays." + design: + status: done + artifact: "openspec/changes/k6b-semantic-integrity-remediation/design.md" + summary: "Token coverage on assessment/v1; ordered non-aliased roles; projector/replay/reconcile full fail-closed integrity." + key_decisions: + - "Require evidence_requirements_satisfied and include canonical coverage in assessment_id" + - "Use rawEvidence order and reject one EvidenceId across distinct strategy roles" + - "Validate canonical inputs, persisted assessments, and complete stored graph payloads" + tasks: + status: ready-for-apply + artifact: "openspec/changes/k6b-semantic-integrity-remediation/tasks.md" + summary: "Backlog TDD en cinco fases y 26 tareas: 22 históricas preservadas y 4 tareas de remediación runtime pendientes; delta estimado 120–220 líneas, total 970–1,370 con size-exception." + key_decisions: + - "Delivery strategy exception-ok; chain strategy size-exception" + - "Unidades ordenadas: contrato, verifier, Assurance Graph, integración/documentación" + - "Remediación focal: tareas 2.8, 3.7, 3.8 y 5.1 añaden únicamente evidencia runtime para los tres hallazgos tasks-gap" + apply: + status: done + artifact: "openspec/changes/k6b-semantic-integrity-remediation/apply-progress.md" + summary: "26/26 tareas verificadas: identidad de assessment y matrices runtime de replay/reconcile; sin cambio de producción." + key_decisions: + - "Legacy assessment/v1 sin cobertura requerida falla cerrada y se regenera; no hay migración" + - "models.yaml preexistente se preservó fuera de scope" + - "Las mutaciones de replay/reconcile recalculan identidades almacenadas para probar las ramas semánticas" + verify: + status: done + verdict: PASS + artifact: "openspec/changes/k6b-semantic-integrity-remediation/verify-report.md" + summary: "PASS: 22/22 MUST scenarios at runtime-test; 26/26 tasks evidenced; focused 82/82 and npm test 2762 pass, 0 fail." + key_decisions: + - "Prior tasks-gap branches now have persistent runtime matrices (2.8, 3.7, 3.8)" + - "Focused 82/82 and full npm test pass; frozen evidence/v2, verification/v2, K1 v1 intact" + - "Assumptions sdd-propose-001 and sdd-spec-001 remain confirmed" + archive: + status: done + artifact: "openspec/changes/k6b-semantic-integrity-remediation/archive-report.md" + summary: "Plan emitido: 3 spec merges, 3 ADR promotions; move pendiente runtime archive-transaction-run.js" + key_decisions: + - "target_before_sha256 desde baseline_fingerprints; prepared-specs bajo change-local" + - "4 WARNING 4R registrados como follow-up no bloqueante en accepted_warnings" + +owner: + author: Manuel Retamozo + branch: fix/k6b-verification-integrity-remediation + +route: + intended_route: standard + actual_route: standard + route_rationale: "First match: foundation/federated/bugfix/brownfield/refactor/hotfix conditions not met; project.status=active and classification=high-risk select standard (sdd-propose → sdd-spec → sdd-design → sdd-tasks → sdd-apply → sdd-verify → sdd-archive; gates: clarify, 4r-review-gate)." + validated: true + validation_errors: [] + +approvals: + - id: intent-briefing-001 + gate: intent-briefing + decision: accepted + source: vscode/askQuestions + accepted_at: "2026-08-27T20:09:00Z" + synthesis: "Remediación focal post-v2.51.0 sobre verifier y Assurance Graph: roles incompatibles no pueden compartir EvidenceId; Strict TDD exige secuencia RED → GREEN (no un Set de etiquetas); cada token de required_evidence queda cubierto y persistido; input.contract.contract_digest === executionGraph.contract_digest antes de strategy; projector, replay y reconcile fallan cerrados ante inputs contradictorios, assessments tampered o stored graphs incompletos. Roadmap: K6b revise, K6c blocked. Fuera de alcance: K6c/K6d/K7/K8, PKI, rediseñar el macro-slice, mutar evidence/v2 o K1 v1 in-place." + scope: "In: integrity semántica de strategy (no aliasing de EvidenceId entre roles incompatibles; orden RED→GREEN / before→after); cobertura token a token de required_evidence persistida en el assessment/binding; binding contract_digest ↔ Execution Graph; projector fail-closed ante canonicalInputs contradictorios o digests ausentes; revalidación de assessments en replay; reconciliación completa del stored graph (nodes, edges, canonical inputs, candidate, kind/schema); tests adversariales; corrección de REQ-006; docs K6b revise / K6c blocked. Out: K6c challenges, K6d complexity, K7 review, K8 attestation, PKI, rediseño del macro-slice K6b, mutación in-place de evidence/v2 o contratos K1 v1." + applies_to: + - change-classification + - id: execution-mode-001 + gate: execution-mode + decision: interactive + source: vscode/askQuestions + accepted_at: "2026-08-27T20:26:00Z" + applies_to: + - sdd-propose + - sdd-spec + - sdd-design + - sdd-tasks + - sdd-apply + - sdd-verify + - sdd-archive + - id: delivery-strategy-001 + gate: delivery-strategy + decision: exception-ok + source: vscode/askQuestions + accepted_at: "2026-08-27T20:26:00Z" + applies_to: + - sdd-tasks + - sdd-apply + - id: architecture-001 + gate: architecture + decision: continue-to-sdd-design + source: vscode/askQuestions + accepted_at: "2026-08-27T20:48:37Z" + applies_to: + - sdd-design + - id: architecture-002 + gate: architecture + decision: continue-to-sdd-tasks + source: vscode/askQuestions + accepted_at: "2026-08-27T20:56:40Z" + applies_to: + - sdd-tasks + - id: review-workload-001 + gate: review-workload + decision: apply-with-size-exception + source: vscode/askQuestions + accepted_at: "2026-08-27T21:00:34Z" + applies_to: + - sdd-apply + - id: testing-001 + gate: testing + decision: continue-to-sdd-verify + source: vscode/askQuestions + accepted_at: "2026-08-27T21:20:56Z" + applies_to: + - sdd-verify + - id: testing-002 + gate: testing + decision: remediate-verify-tasks-gap + source: vscode/askQuestions + accepted_at: "2026-08-27T21:31:36Z" + applies_to: + - sdd-tasks + - id: review-workload-002 + gate: review-workload + decision: apply-remediation-with-size-exception + source: vscode/askQuestions + accepted_at: "2026-08-27T21:35:06Z" + applies_to: + - sdd-apply + - id: execution-mode-002 + gate: execution-mode + decision: auto + source: vscode/askQuestions + accepted_at: "2026-08-27T22:04:00Z" + applies_to: + - sdd-archive + +assumptions: + - id: sdd-propose-001 + phase: sdd-propose + statement: "The persistable coverage field on assessment/binding is additive and design-owned (working name required_evidence_ref); evidence/v2 and K1 v1 stay frozen." + reversibility: high + basis: "User intent named required_evidence_ref as an example; predecessor treated assessment schema $id as design-owned; kernel-contract-schemas-027 already owns the additive family." + recorded_at: "2026-08-27T20:28:00Z" + status: confirmed + - id: sdd-spec-001 + phase: sdd-spec + statement: "The additive coverage field on assessment/binding is schema-required; omitting it MUST fail validation." + reversibility: high + basis: "Proposal requires persisted coverage on assessment/binding; exact field name remains design-owned." + recorded_at: "2026-08-27T20:40:00Z" + status: confirmed + +baseline_fingerprints: + independent-verification: "sha256:3207ce8b5b280472b9b505378cf91df22b1ae22e007adb9dbf8a173405c5b0a0" + assurance-graph: "sha256:81f10f2046ae5519aead34f0f945275e99bfefbc03f9a898d80ce8bcbc437f56" + kernel-contract-schemas: "sha256:0f6c1f85aaff2f9b7a60320a66830bbc23feeb01a29ce4e6a8ef8f5d59ef4cec" + +gates: + 4r-review-gate: {"status":"done","schema_version":1,"classification":"high-risk","depth":{"review":"strict"},"escalation_reason":null,"selected_specialists":["risk","reliability","resilience","readability"],"generalist":{"status":"needs-specialist","specialists":["risk","reliability"],"reason":"signals=design-risk,diff-auth-permission,metadata-runtime;dimensions=risk,reliability"},"evidence":{"schema_version":1,"fingerprint":"sha256:35db40e519d337da13828b16e042956c911cae1d2f3c0239d1502d8b5c8a59ce"},"lineage_status":"approved","lineage_id":"sha256:8c6808007fb67e490cfd80cf38a8d40cef42bcf0f0e6b1a87bd73c7ee661906d","lineage_revision":9,"lineage_artifact":"openspec/changes/k6b-semantic-integrity-remediation/.4r/lineage.json","findings_summary":"0 BLOCKER, 0 CRITICAL, 4 WARNING, 0 SUGGESTION","archive_allowed":true,"correction_budget_limit_lines":200,"terminal_reason":"no-unresolved-blocking-findings"} diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/tasks.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/tasks.md new file mode 100644 index 00000000..da4f8597 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/tasks.md @@ -0,0 +1,91 @@ +# Tasks: k6b Semantic Integrity Remediation + +## Spec/Design Reconciliation + +| Requirement / Scenario | Priority | Design Allocation | Status | Notes | +|---|---|---|---|---| +| REQ-independent-verification-008 / contract digest mismatch | MUST | `scripts/lib/independent-verifier/bindings.js`, ordered facade in `index.js`; adversarial verifier test | covered-by-design | Gate runs before strategy and emits no verdict. | +| REQ-independent-verification-005 / MUST token subset, unknown obligation, wrong node, partial coverage | MUST | `evidence.js`, `obligation-coverage.js`, `assessment.js`; coverage tests | covered-by-design | `required_evidence` is checked against sorted persisted satisfied tokens per binding. | +| REQ-independent-verification-006 / distinct assessment bindings, no role aliasing, temporal order | MUST | `strategy-policy.js`, `assessment.js`, facade; strategy/identity tests | covered-by-design | Raw evidence order is authoritative; evidence/v2 remains unchanged. | +| REQ-kernel-contract-schemas-027 / additive schema, coverage, closed family, frozen pins | MUST | `schemas/kernel/assessment/v1.schema.json`, `contract-claims.json`, fixtures and schema tests | covered-by-design | Includes complete, omitted-field, verdict/cross-family, four-role and pin checks. | +| REQ-assurance-graph-007 / contradictory or null canonical inputs | MUST | `scripts/lib/assurance-graph/projector.js`; projector tests | covered-by-design | Required SHA-256 digests are validated before graph-id construction. | +| REQ-assurance-graph-006 / replay assessment revalidation | MUST | `assessment.js`, `scripts/lib/assurance-graph/index.js`; replay integration tests | covered-by-design | Revalidates schema, identity, candidate/policy, evidence, obligation and node bindings. | +| REQ-assurance-graph-008 / complete stored-payload reconciliation | MUST | `scripts/lib/assurance-graph/index.js`; reconcile tamper tests | covered-by-design | Recomputes stored graph id and compares nodes, canonical inputs and identity fields. | + +### Reconciliation Verdict +- MUST coverage: complete +- SHOULD/MAY gaps: none +- Ambiguities to track: none + +## Review Workload Forecast + +| Field | Value | +|---|---| +| Estimated changed lines | 970–1,370 (incremental remediation: 120–220) | +| 400-line budget risk | High | +| Chained PRs recommended | Yes | +| Suggested split | Single maintainer-approved PR with explicit `size:exception`; implement in dependency-ordered work units. | +| Delivery strategy | exception-ok | +| Chain strategy | size-exception | + +Decision needed before apply: No +Chained PRs recommended: Yes +Chain strategy: size-exception +400-line budget risk: High + +## Suggested Work Units + +| Unit | Goal | Likely PR | Notes | +|---|---|---|---| +| 1 | Extend assessment contract and canonical identity | PR 1 (size exception) | Schema, claims, assessment id/validation, fixtures; verify frozen pins. | +| 2 | Enforce verifier binding, strategy order and MUST token coverage | PR 1 (size exception) | RED tests first, then implementation and refactor; depends on Unit 1. | +| 3 | Harden projector, replay and reconcile | PR 1 (size exception) | Canonical-input validation and full stored-payload checks; depends on Units 1–2. | +| 4 | Integrate adversarial/e2e tests and roadmap status | PR 1 (size exception) | Run full `npm test`; docs remain within K6b scope. | +| 5 | Close verify evidence gaps with persistent runtime tests | PR 1 (size exception) | Depends on existing verifier and Assurance Graph behavior; no production-code or contract changes. | + +## Checklist Status Legend + +- `[ ]` Not implemented yet +- `[~]` Implemented but not yet verified locally +- `[x]` Implemented and verified locally + +## Phase 1: Assessment Contract and Fixtures + +- [x] 1.1 Update `schemas/kernel/assessment/v1.schema.json` with required closed-array `evidence_requirements_satisfied`, preserving `additionalProperties: false` and excluding `verdict` [REQ-kernel-contract-schemas-027] +- [x] 1.2 Add the additive assessment claim to `schemas/kernel/contract-claims.json` without changing its manifest `$id` or frozen K1 claims [REQ-kernel-contract-schemas-027] +- [x] 1.3 Update/create `schemas/kernel/assessment/fixtures/valid/*.json` and `invalid/*.json` for complete coverage, omitted coverage, verdict/cross-family substitution, and four distinct roles [REQ-kernel-contract-schemas-027] +- [x] 1.4 Write RED tests in `scripts/lib/k6b-schema-fixtures.test.js` for schema identity, required coverage, pairwise assessment ids, and unchanged evidence/v2, verification/v2, and K1 pins [REQ-kernel-contract-schemas-027] +- [x] 1.5 Implement canonical assessment validation and `computeAssessmentId` coverage sorting/deduplication in `scripts/lib/independent-verifier/assessment.js`; GREEN then REFACTOR the tests from 1.4 [REQ-independent-verification-006, REQ-kernel-contract-schemas-027] + +## Phase 2: Verifier Semantics and Coverage + +- [x] 2.1 Add RED tests to `scripts/lib/independent-verifier/*.test.js` proving contract C1/C2 mismatch fails before strategy and produces no PASS [REQ-independent-verification-008] +- [x] 2.2 Implement the canonical contract binding gate in `scripts/lib/independent-verifier/bindings.js` and ordered facade in `scripts/lib/independent-verifier/index.js`; GREEN and REFACTOR [REQ-independent-verification-008] +- [x] 2.3 Add RED tests for same EvidenceId across incompatible roles, GREEN-before-RED, RED-after-PATCH, and valid ordered distinct ids [REQ-independent-verification-006] +- [x] 2.4 Implement normalized role identity and temporal policy in `scripts/lib/independent-verifier/strategy-policy.js`, retaining rawEvidence order; GREEN and REFACTOR [REQ-independent-verification-006] +- [x] 2.5 Add RED tests for unknown obligations, non-implementing nodes, no evidence, and `[A,B]` with only A satisfied [REQ-independent-verification-005] +- [x] 2.6 Implement binding coverage normalization in `scripts/lib/independent-verifier/evidence.js` and token-subset MUST walk in `obligation-coverage.js`; persist coverage per assessment and GREEN/REFACTOR [REQ-independent-verification-005] +- [x] 2.7 Update the verifier facade tests to prove gate order (bindings → strategy → MUST walk), distinct tuple assessments, and unchanged `evidence/v2`/`verification/v2` payload shape [REQ-independent-verification-005, REQ-independent-verification-006, REQ-independent-verification-008] +- [x] 2.8 RED: extend `scripts/lib/independent-verifier/assessment.test.js` to vary `evidence_id` and `obligation_id` independently and assert distinct assessment identities for each tuple; GREEN/REFACTOR only if the focused runtime test exposes an implementation gap [REQ-independent-verification-006] + +## Phase 3: Assurance Graph Integrity + +- [x] 3.1 Add RED projector tests in `scripts/lib/assurance-graph/index.test.js` for contradictory Graph/contract/policy digests and null/absent required canonical digests [REQ-assurance-graph-007] +- [x] 3.2 Implement strict resolved canonical-input validation in `scripts/lib/assurance-graph/projector.js` before graph-id preimage construction; GREEN and REFACTOR [REQ-assurance-graph-007] +- [x] 3.3 Add RED replay cases for malformed/tampered assessment schema, assessment_id, candidate, policy, evidence reference, obligation, node, node_id and coverage [REQ-assurance-graph-006] +- [x] 3.4 Implement persistable assessment revalidation and token-level coverage checks in `scripts/lib/assurance-graph/index.js`, using shared `assessment.js` helpers; GREEN and REFACTOR [REQ-assurance-graph-006] +- [x] 3.5 Add RED reconcile cases for tampered nodes, canonical inputs, candidate_id, kind/schema, declared graph_id and stored payload identity [REQ-assurance-graph-008] +- [x] 3.6 Implement stored-payload graph-id recomputation and full payload comparison in `scripts/lib/assurance-graph/index.js`; GREEN and REFACTOR [REQ-assurance-graph-008] +- [x] 3.7 RED: extend `scripts/lib/assurance-graph/index.test.js` with persistent replay mutations for malformed assessment schema/coverage, candidate/policy mismatch, missing evidence, unknown obligation, non-implementing node, and `node_id` mismatch; GREEN/REFACTOR only if runtime tests expose an implementation gap [REQ-assurance-graph-006] +- [x] 3.8 RED: extend `scripts/lib/assurance-graph/index.test.js` with persistent reconcile mutations for stored `canonical_inputs`, `candidate_id`, and kind/schema divergence, asserting `GRAPH_DIVERGENCE`; GREEN/REFACTOR only if runtime tests expose an implementation gap [REQ-assurance-graph-008] + +## Phase 4: Integration, Documentation and Verification + +- [x] 4.1 Extend `scripts/k6b-verifier-assurance-graph-e2e.test.js` with a valid token-complete verify/project/replay/reconcile path, deterministic second projection, and stored mutation failures [REQ-independent-verification-005, REQ-independent-verification-006, REQ-assurance-graph-006, REQ-assurance-graph-008] +- [x] 4.2 Update `docs/architecture/harness-evolution.md` and `docs/roadmaps/harness-evolution.md` to mark K6b `revise` and K6c `blocked-by-K6b-remediation` until archive [REQ-assurance-graph-008] +- [x] 4.3 Run `npm test` and inspect the complete native test output; resolve only failures attributable to the scoped B1–B3/H1–H3 remediation, preserving frozen K1 and v2 bytes [REQ-independent-verification-005, REQ-independent-verification-006, REQ-independent-verification-008, REQ-assurance-graph-006, REQ-assurance-graph-007, REQ-assurance-graph-008, REQ-kernel-contract-schemas-027] +- [x] 4.4 Confirm no migration is attempted for legacy partial `assessment/v1` records and record regeneration/rollback behavior in apply progress [REQ-kernel-contract-schemas-027] + +## Phase 5: Verify-Gap Runtime Evidence + +- [x] 5.1 Run the focused verifier and Assurance Graph suites plus `npm test`; record the new runtime evidence and leave any implementation mismatch for the routed phase [REQ-independent-verification-006, REQ-assurance-graph-006, REQ-assurance-graph-008] diff --git a/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/verify-report.md b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/verify-report.md new file mode 100644 index 00000000..d8b5829d --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-semantic-integrity-remediation/verify-report.md @@ -0,0 +1,153 @@ +## Verification Report + +**Change**: k6b-semantic-integrity-remediation +**Version**: 2.51.0 +**Mode**: Standard (TDD focused) + +Re-verification after the prior FAIL (`origin: tasks-gap`). Authoritative checklist is now 26 tasks (22 historical + remediating 2.8, 3.7, 3.8, and 5.1). Lineage router (`getLineageNextAction`) returned `run-discovery` / `no-active-lineage`; this run executed the full discovery pipeline. No `verify_lineage` was opened because no BLOCKER/CRITICAL findings remain. + +### Completeness + +| Metric | Value | +|--------|------:| +| Tasks total | 26 | +| Tasks complete | 26 | +| Tasks incomplete | 0 | + +All 26 checklist items in `tasks.md` are marked `[x]`. Independent runtime evidence now covers the three previously overclaimed tasks via 2.8, 3.7, and 3.8; task 5.1 records the focused plus full-suite execution. + +### Build & Tests Execution + +**Build**: N/A — `rules.verify.build_command` is empty and this CommonJS repository declares no independent type-check/build gate. + +**Focused tests**: ✅ 82 passed / 0 failed / 0 skipped + +```text +node --test scripts/lib/k6b-schema-fixtures.test.js scripts/lib/independent-verifier/assessment.test.js scripts/lib/independent-verifier/obligation-coverage.test.js scripts/lib/independent-verifier/index.test.js scripts/lib/assurance-graph/index.test.js scripts/k6b-verifier-assurance-graph-e2e.test.js + +exit code: 0 +tests: 82 +pass: 82 +fail: 0 +skipped: 0 +duration_ms: 194.1959 +``` + +The prior FAIL recorded 79/79 on this command. The +3 tests are the persistent identity and adversarial matrices added by tasks 2.8, 3.7, and 3.8. + +**Full regression suite**: ✅ 2762 passed / 0 failed / 2 skipped + +```text +npm test +(raw: node --test scripts/**/*.test.js, then target generate/validate via scripts/check.js) + +exit code: 0 +Native Node tests: tests 2764, pass 2762, fail 0, skipped 2 +terminal result: All checks passed. +``` + +**Static repository checks**: ✅ Passed + +```text +git diff --check +exit code: 0 + +git diff --quiet -- schemas/kernel/evidence schemas/kernel/verification scripts/lib/lifecycle-kernel/k1-compat.js +exit code: 0 +``` + +Git emitted prospective LF/CRLF normalization notices for several modified files, but `git diff --check` found no whitespace error. Frozen `evidence/v2`, `verification/v2`, and K1 v1 bytes are unmodified. `models.yaml` has an independent pre-existing one-line model selection change and is excluded from this change's functional evidence. + +**Manual verification**: source/test inspection performed; no separate UI or manual runtime workflow applies. + +**Coverage**: ➖ Not available (`testing.coverage.available: false`). + +**Quality gates**: no active `quality_gates:` policy is declared; Step 9a is a strict no-op and no `gates.quality-gates` audit block is written. + +### Spec Compliance Matrix + +| Requirement | Scenario | Evidence Level | Source | Result | Notes | +|-------------|----------|----------------|--------|--------|-------| +| REQ-assurance-graph-007 | Graph contract contradicts canonicalInputs | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > contradictory canonical inputs | PASS | Contract, policy, and Execution Graph digest contradictions return `GRAPH_DIVERGENCE`. | +| REQ-assurance-graph-007 | Null required canonical digest is not fingerprinted | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > missing required canonical digest | PASS | Missing policy digest fails before graph-id construction. | +| REQ-assurance-graph-008 | Tampered stored nodes fail closed | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > replay and reconcile tampering | PASS | Added stored node returns `GRAPH_DIVERGENCE`. | +| REQ-assurance-graph-008 | Stored payload or identity fields diverge | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > stored identity mutations | PASS | Task 3.8 mutates stored `canonical_inputs`, `candidate_id`, `kind`, and `schema_version` after recomputing stored `graph_id`; each returns `GRAPH_DIVERGENCE`. | +| REQ-assurance-graph-006 | Replay from persisted outputs yields the same graph | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` and K6b E2E | PASS | Replayed `graph_id` and edges are byte-identical. | +| REQ-assurance-graph-006 | Tampered assessment_id fails replay | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > assessment tampering | PASS | Tampered identity returns `GRAPH_DIVERGENCE`. | +| REQ-assurance-graph-006 | Assessment fails schema, candidate, or policy revalidation | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > replay binding mutations | PASS | Task 3.7 covers malformed schema, coverage outside requirements, candidate mismatch, and policy mismatch; each returns `GRAPH_DIVERGENCE`. | +| REQ-assurance-graph-006 | Missing evidence or non-implementing node fails replay | `runtime-test` | `scripts/lib/assurance-graph/index.test.js` > replay binding mutations | PASS | Task 3.7 covers missing evidence, unknown obligation, non-implementing node, and `node_id` mismatch; each returns `GRAPH_DIVERGENCE`. | +| REQ-independent-verification-008 | Contract digest mismatch fails before strategy | `runtime-test` | `scripts/lib/independent-verifier/index.test.js` > contract digest mismatch | PASS | Returns `BINDING_MISMATCH` with no verification verdict. | +| REQ-independent-verification-005 | MUST without admissible evidence fails closed | `runtime-test` | verifier and obligation-coverage tests | PASS | Returns `UNFULFILLED_MUST` and identifies the obligation. | +| REQ-independent-verification-005 | Nonexistent obligation_id fails closed | `runtime-test` | verifier and obligation-coverage tests | PASS | Returns `UNKNOWN_OBLIGATION_ID`. | +| REQ-independent-verification-005 | Wrong implementing node fails closed | `runtime-test` | verifier and obligation-coverage tests | PASS | Returns `WRONG_IMPLEMENTING_NODE`. | +| REQ-independent-verification-005 | Partial required_evidence coverage fails closed | `runtime-test` | `scripts/lib/independent-verifier/obligation-coverage.test.js` > token subset coverage | PASS | `[A,B]` with only A returns `UNFULFILLED_MUST`; complete union persists per-assessment coverage. | +| REQ-independent-verification-006 | Same EvidenceId as RED and GREEN fails closed | `runtime-test` | `scripts/lib/independent-verifier/index.test.js` > incompatible role aliasing | PASS | Shared observation across distinct strategy roles returns `STRATEGY_EVIDENCE_ALIAS`. | +| REQ-independent-verification-006 | GREEN before RED fails closed | `runtime-test` | `scripts/lib/independent-verifier/index.test.js` > strict-tdd role order | PASS | Returns `STRATEGY_SEQUENCE_VIOLATION`. | +| REQ-independent-verification-006 | RED after PATCH fails closed | `runtime-test` | `scripts/lib/independent-verifier/index.test.js` > bug role order | PASS | Returns `STRATEGY_SEQUENCE_VIOLATION`. | +| REQ-independent-verification-006 | Distinct tuples yield distinct assessment identities | `runtime-test` | `scripts/lib/independent-verifier/assessment.test.js` > independent `evidence_id`/`obligation_id` | PASS | Task 2.8 varies `evidence_id` and `obligation_id` independently (role held constant) and asserts three pairwise-distinct `assessment_id` values. | +| REQ-kernel-contract-schemas-027 | Valid assessment fixture passes | `runtime-test` | `scripts/lib/k6b-schema-fixtures.test.js` | PASS | Complete coverage field validates. | +| REQ-kernel-contract-schemas-027 | Cross-family substitution and verdict fail closed | `runtime-test` | `scripts/lib/k6b-schema-fixtures.test.js` | PASS | Assessment rejects verdict and does not validate as evidence/v2 or verification/v2. | +| REQ-kernel-contract-schemas-027 | Four-role assessments remain distinct under schema | `runtime-test` | schema fixture and assessment unit tests | PASS | Four role-distinct records remain schema-valid and have pairwise assessment ids. | +| REQ-kernel-contract-schemas-027 | Assessment without coverage fails closed | `runtime-test` | `scripts/lib/k6b-schema-fixtures.test.js` > missing coverage fixture | PASS | Omission of `evidence_requirements_satisfied` is rejected. | +| REQ-kernel-contract-schemas-027 | Evidence v2, verification v2, and K1 v1 remain frozen | `runtime-test` + `static-proof` | K6b schema tests; `git diff --quiet` | PASS | Pin assertions pass and relevant schema/fixture/pin paths have no working-tree delta. | + +**Compliance summary**: 22/22 scenarios satisfy the required evidence level (`runtime-test`, with frozen-pin `static-proof` on REQ-kernel-contract-schemas-027). + +### Correctness (Static Evidence) + +| Requirement | Status | Notes | +|------------|--------|-------| +| REQ-independent-verification-008 | ✅ Implemented | Contract digest equality is enforced in `validateBindings` before strategy selection. | +| REQ-independent-verification-005 | ✅ Implemented | Required tokens are unioned per obligation and missing tokens fail closed. | +| REQ-independent-verification-006 | ✅ Implemented | Role alias/order checks and tuple identity (role, `evidence_id`, `obligation_id`) are now proven at runtime. | +| REQ-kernel-contract-schemas-027 | ✅ Implemented | Required closed coverage field, claims, fixtures, frozen families. | +| REQ-assurance-graph-007 | ✅ Implemented | Required digests and contradictions fail before fingerprinting. | +| REQ-assurance-graph-006 | ✅ Implemented | Replay revalidates schema, identity, candidate/policy, evidence, obligation, and node bindings. | +| REQ-assurance-graph-008 | ✅ Implemented | Full stored-payload comparison includes nodes, canonical inputs, candidate, and kind/schema. | + +### Coherence (Design) + +| Decision | Followed? | Notes | +|----------|-----------|-------| +| ADR-001: required canonical coverage on assessment/v1 | ✅ Yes | Field is schema-required, canonicalized, and included in assessment identity; legacy omission fails closed. | +| ADR-002: ordered, non-aliased strategy evidence | ✅ Yes | Raw order drives strict-tdd/bug sequencing and one id cannot cover distinct roles. | +| ADR-003: canonical integrity across project/replay/reconcile | ✅ Yes | Shared helpers and fail-closed paths are now covered by persistent replay and reconcile adversarial matrices. | + +No production-design deviation was found. Remediation tasks added tests only; apply progress records no production-code correction in batch 2. + +### Traceability Matrix + +| REQ | Tasks | Commits | Tests | Status | +|-----|-------|---------|-------|--------| +| REQ-independent-verification-008 | 2.1–2.2, 2.7, 4.3 | none (working tree) | verifier contract-digest test | OK | +| REQ-independent-verification-005 | 2.5–2.7, 4.1, 4.3 | none (working tree) | verifier, obligation coverage, E2E | OK | +| REQ-independent-verification-006 | 1.5, 2.3–2.4, 2.7, 2.8, 4.1, 4.3, 5.1 | none (working tree) | assessment identity + verifier strategy tests | OK | +| REQ-kernel-contract-schemas-027 | 1.1–1.5, 4.3–4.4 | none (working tree) | K6b schema fixtures + assessment tests | OK | +| REQ-assurance-graph-007 | 3.1–3.2, 4.3 | none (working tree) | projector tests | OK | +| REQ-assurance-graph-006 | 3.3–3.4, 3.7, 4.1, 4.3, 5.1 | none (working tree) | replay tests + E2E | OK | +| REQ-assurance-graph-008 | 3.5–3.6, 3.8, 4.1, 4.3, 5.1 | none (working tree) | reconcile tests + E2E | OK | + +Traceability trailers are advisory because no active `traceability:` policy is declared. The absence of commits does not itself add a finding in this working-tree verification. + +### Assumption Reconciliation + +| id | statement | reversibility | outcome | +|----|-----------|----------------|---------| +| sdd-propose-001 | Coverage stays additive on assessment/binding; evidence/v2 and K1 v1 remain frozen. | high | confirmed | +| sdd-spec-001 | Coverage is schema-required and omission fails validation. | high | confirmed | + +### Issues Found + +**CRITICAL**: None. + +**WARNING**: None. + +**SUGGESTION** + +- Three historical BLOCKER entries for this change remain in `openspec/memory/known-issues.md` from the prior FAIL. Step 10b does not rewrite or close them on PASS; they are an audit trail of the remediated tasks-gap, not active verify findings. + +### Verdict + +**PASS** + +All 26 tasks are complete, all 22 MUST scenarios now have `runtime-test` (or accepted frozen-pin `static-proof`) evidence, focused 82/82 and full `npm test` passed, and frozen evidence/v2, verification/v2, and K1 v1 bytes are unmodified. Route to `sdd-archive`. diff --git a/openspec/config.yaml b/openspec/config.yaml index 5f39e6f1..a18f725d 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -11,7 +11,7 @@ context: | project: name: ospec-workflow - version: 2.51.0 + version: 2.52.0 status: active artifact_store: diff --git a/openspec/specs/assurance-graph/spec.md b/openspec/specs/assurance-graph/spec.md index eb39af97..2c78bbde 100644 --- a/openspec/specs/assurance-graph/spec.md +++ b/openspec/specs/assurance-graph/spec.md @@ -9,7 +9,7 @@ invalidation on successor preserves independent evidence. ## Requirements -### Requirement: Derived Projection Not Authority {#REQ-assurance-graph-001} +### Requirement:### Requirement: Derived Projection Not Authority {#REQ-assurance-graph-001} The Assurance Graph MUST be derived from canonical OpenSpec artifacts, Git bytes, the frozen Candidate, the Execution Graph, evidence nodes, verification @@ -42,7 +42,7 @@ treat Evaluation Attestation or Delivery Authorization as authority. - WHEN reconciliation runs against the stored graph - THEN validation MUST fail closed with `GRAPH_DIVERGENCE` -### Requirement: Reproducible Digest And K6b Edges {#REQ-assurance-graph-002} +### Requirement:### Requirement: Reproducible Digest And K6b Edges {#REQ-assurance-graph-002} Identical persistable canonical inputs MUST produce the same graph digest and the same `AssuranceEdge` set. `graph_id` MUST fingerprint at least: @@ -84,7 +84,7 @@ findings, K8 attestation, or K10 authorization as authoritative subjects. - THEN the resulting `graph_id` MUST differ - AND reconciliation against the prior graph MUST fail closed with `GRAPH_DIVERGENCE` -### Requirement: Selective Invalidation Closure {#REQ-assurance-graph-003} +### Requirement:### Requirement: Selective Invalidation Closure {#REQ-assurance-graph-003} When a Candidate successor appears or a source subject changes, the system MUST compute the dependent closure over `invalidates`, `derived-from`, @@ -108,7 +108,7 @@ a transitive `invalidates` edge. - THEN the verifier MUST fail closed - AND MUST NOT treat E as satisfying evidence -### Requirement: Non-Authoritative Equivalence Manifest {#REQ-assurance-graph-004} +### Requirement:### Requirement: Non-Authoritative Equivalence Manifest {#REQ-assurance-graph-004} The system MAY emit an equivalence manifest bound to the graph digest and `CandidateId` for later K9 evaluation. The manifest MUST NOT promote @@ -128,7 +128,7 @@ authorization. - WHEN validated against CandidateEvaluationAttestation or DeliveryAuthorization schemas - THEN validation MUST fail closed -### Requirement: Forbidden Subjects Matched By Kind And Namespace {#REQ-assurance-graph-005} +### Requirement:### Requirement: Forbidden Subjects Matched By Kind And Namespace {#REQ-assurance-graph-005} `rejectForbidden` MUST accept or reject subjects using structured `kind` and `namespace` (or equivalent typed fields). It MUST NOT reject a subject because @@ -158,11 +158,85 @@ input digests, projected nodes and edges, evidence records, verification records, and assessment/binding records that carry `obligation_id`. Replay MUST NOT require ephemeral projector fields. Consumers MUST NOT reinvent `obligation_id` values from vanished fields. `satisfies` edges MUST be -rebuildable from persistable assessments. +rebuildable from persistable assessments. `replayAssuranceGraph` MUST +revalidate each persisted assessment before accepting the replay: schema +validity; recomputed assessment identity; `candidate_id` match; bound +policy-snapshot identity; referenced evidence exists; `obligation_id` is in +the graph; the bound node implements that obligation; and persistable +`node_id` matches the evidence record. Any check failure MUST fail closed +with `GRAPH_DIVERGENCE` or as an invalid assessment. A tampered +`assessment_id` MUST NOT replay as valid. +(Previously: replay compared recomputed graph_id and edges without revalidating persisted assessments.) #### Scenario: Replay from persisted outputs yields the same graph - GIVEN a previously projected graph plus persisted evidence, verification, assessments, and canonical input digests - AND no ephemeral `obligation_ids` on the original projector call +- AND every persisted assessment passes revalidation - WHEN the graph is replayed from those persistable outputs only - THEN the recomputed `graph_id` and edge set MUST be byte-identical to the stored graph + +#### Scenario: Tampered assessment_id fails replay + +- GIVEN persisted assessments plus a stored graph that would otherwise replay +- AND one assessment whose `assessment_id` does not match the identity recomputed from its persistable fields +- WHEN `replayAssuranceGraph` runs +- THEN replay MUST fail closed with `GRAPH_DIVERGENCE` or as an invalid assessment + +#### Scenario: Assessment fails schema, candidate, or policy revalidation + +- GIVEN a persisted assessment that fails schema validation, whose `candidate_id` does not match the graph subject, or whose bound policy-snapshot identity does not match the graph +- WHEN `replayAssuranceGraph` runs +- THEN replay MUST fail closed with `GRAPH_DIVERGENCE` or as an invalid assessment + +#### Scenario: Assessment bound to missing evidence or non-implementing node fails replay + +- GIVEN a persisted assessment whose `evidence_id` does not exist, whose `obligation_id` is absent from the graph, whose node does not implement that obligation, or whose `node_id` disagrees with the evidence record +- WHEN `replayAssuranceGraph` runs +- THEN replay MUST fail closed with `GRAPH_DIVERGENCE` or as an invalid assessment + +### Requirement: Projector Fail-Closed On Contradictory Canonical Inputs {#REQ-assurance-graph-007} + +`projectAssuranceGraph` MUST fail closed with `GRAPH_DIVERGENCE` when supplied +`canonicalInputs` contradict the Graph, contract, or policy they claim to +project. Required canonical input digests MUST be present and non-null. The +projector MUST NOT fingerprint a null or absent digest for any required +canonical input in the `graph_id` preimage. Contradiction or a null required +digest MUST NOT yield a successful projection. + +#### Scenario: Graph contract contradicts canonicalInputs + +- GIVEN an Execution Graph whose contract digest is C1 +- AND `canonicalInputs` whose contract digest is C2, where C1 ≠ C2 +- WHEN `projectAssuranceGraph` runs +- THEN projection MUST fail closed with `GRAPH_DIVERGENCE` +- AND MUST NOT emit a graph whose `graph_id` fingerprints the contradictory pair + +#### Scenario: Null required canonical digest is not fingerprinted + +- GIVEN a required canonical input whose digest is null or absent +- WHEN `projectAssuranceGraph` runs +- THEN projection MUST fail closed +- AND MUST NOT produce a `graph_id` that fingerprints a null digest for that input + +### Requirement: Reconcile Stored Payload Divergence {#REQ-assurance-graph-008} + +`reconcileAssuranceGraph` MUST detect stored-graph divergence beyond equality +of `graph_id` and edges. It MUST fail closed with `GRAPH_DIVERGENCE` when +stored `nodes`, `canonical_inputs`, `candidate_id`, or kind/schema diverge +from the recomputed projection, or when `graph_id` recomputed from the stored +payload does not match the stored `graph_id`. Comparing only stored +`graph_id` plus edges MUST NOT be sufficient to declare reconciliation OK. + +#### Scenario: Tampered stored nodes fail closed + +- GIVEN a stored Assurance Graph whose `graph_id` and edges match a recomputation +- AND whose persistable `nodes` have been altered +- WHEN `reconcileAssuranceGraph` runs +- THEN validation MUST fail closed with `GRAPH_DIVERGENCE` + +#### Scenario: Stored payload or identity fields diverge + +- GIVEN a stored Assurance Graph whose `candidate_id`, persistable `canonical_inputs`, or kind/schema differ from the current projection, or whose declared `graph_id` does not equal the digest recomputed from its stored payload +- WHEN `reconcileAssuranceGraph` runs +- THEN validation MUST fail closed with `GRAPH_DIVERGENCE` diff --git a/openspec/specs/independent-verification/spec.md b/openspec/specs/independent-verification/spec.md index 2399d93f..c8d86552 100644 --- a/openspec/specs/independent-verification/spec.md +++ b/openspec/specs/independent-verification/spec.md @@ -9,7 +9,7 @@ evidence strategy until equivalence is proven; it does not rewrite SDD ## Requirements -### Requirement: Frozen Candidate Subject And Canonical Bindings {#REQ-independent-verification-001} +### Requirement:### Requirement: Frozen Candidate Subject And Canonical Bindings {#REQ-independent-verification-001} The verifier MUST accept only a frozen `CandidateId` as subject, plus contract, Execution Graph, repository bytes, and raw evidence. It MUST validate canonical @@ -36,7 +36,7 @@ for a missing structured subject or binding. - WHEN the verifier starts - THEN verification MUST fail closed before strategy evaluation -### Requirement: Evidence Strategies And Strict TDD Fallback {#REQ-independent-verification-002} +### Requirement:### Requirement: Evidence Strategies And Strict TDD Fallback {#REQ-independent-verification-002} The verifier MUST select exactly one strategy from `bug | feature | refactor | migration | config-docs`. Each strategy MUST @@ -74,7 +74,7 @@ fallback. - THEN it MUST apply Strict TDD as the evidence strategy - AND MUST leave `testing.tdd_mode` unchanged -### Requirement: Provenance Sufficiency And Fail-Closed Evidence {#REQ-independent-verification-003} +### Requirement:### Requirement: Provenance Sufficiency And Fail-Closed Evidence {#REQ-independent-verification-003} Every evidence node MUST declare provenance as exactly one of `runtime-observed | host-attested | tool-produced | model-reported | @@ -123,7 +123,7 @@ class. - THEN verification MUST fail closed - AND MUST NOT accept the payload string as a strong class -### Requirement: Verdict Is Not Evidence {#REQ-independent-verification-004} +### Requirement:### Requirement: Verdict Is Not Evidence {#REQ-independent-verification-004} Verification MUST emit a verification record bound to the frozen `CandidateId` with verdict `PASS | PASS WITH WARNINGS | FAIL`. Evidence records MUST NOT @@ -157,14 +157,19 @@ fallback. After strategy evaluation, the verifier MUST walk every Obligation Manifest item with criticality `must` that is not an approved `deferred` record (`reason` and `approved_by`). For each such obligation, a `PASS` or -`PASS WITH WARNINGS` verdict MUST require admissible evidence covering -`required_evidence`, persistably bound to that `obligation_id` and to a -`node_id` listed in `implemented_by`. Strategy role shape alone MUST NOT -satisfy the graph. An `obligation_id` absent from the manifest MUST fail +`PASS WITH WARNINGS` verdict MUST require `required_evidence` ⊆ persistable +satisfied tokens, each persistably bound to that `obligation_id` and to a +`node_id` listed in `implemented_by`. Coverage MUST be persisted on the +assessment/binding record (additive field; exact name design-owned). A +non-empty evidence list, unique-sort of `evidence_ids`, or existential +binding MUST NOT substitute for that subset. Strategy role shape alone MUST +NOT satisfy the graph. An `obligation_id` absent from the manifest MUST fail closed. Evidence bound to a node that does not implement the obligation MUST -fail closed. A MUST without admissible evidence MUST fail closed and MUST -identify the unfulfilled `obligation_id`. The verifier MUST consume persistable -manifest `obligation_id` values; it MUST NOT invent them from vanished fields. +fail closed. A MUST whose `required_evidence` is not a subset of satisfied +tokens MUST fail closed and MUST identify the unfulfilled `obligation_id`. +The verifier MUST consume persistable manifest `obligation_id` values; it +MUST NOT invent them from vanished fields. +(Previously: coverage required admissible evidence "covering" required_evidence without subset semantics or persistable satisfied tokens.) #### Scenario: MUST without admissible evidence fails closed @@ -190,26 +195,60 @@ manifest `obligation_id` values; it MUST NOT invent them from vanished fields. - THEN it MUST fail closed - AND MUST NOT treat the evidence as satisfying that obligation +#### Scenario: Partial required_evidence coverage fails closed + +- GIVEN a non-deferred MUST obligation with `required_evidence` equal to `[A, B]` +- AND admissible evidence that satisfies only token A +- WHEN the verifier evaluates obligation coverage +- THEN it MUST fail closed identifying that `obligation_id` +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + ### Requirement: Persistable Assessment Binding Distinct From Evidence {#REQ-independent-verification-006} The verifier MUST persist an additive assessment/binding record for each evaluated tuple of `evidence_id`, `role`, `obligation_id`, `node_id`, and bound policy-snapshot identity. Assessment identity MUST include `role` and -`obligation_id`. `evidence/v2` MUST remain the observation record and MUST NOT -be mutated to carry `role` or `obligation_id`. The same `EvidenceId` used as -four roles MUST yield four distinct assessment identities. Unique-sort of -`verification.evidence_ids` MUST NOT be the assessment identity and MUST NOT -hide distinct role or obligation bindings. - -#### Scenario: Same EvidenceId used as four roles yields four assessments +`obligation_id` and MUST be distinct per `(evidence_id, role, obligation_id)`. +`evidence/v2` MUST remain the observation record and MUST NOT be mutated to +carry `role` or `obligation_id`. Incompatible strategy roles MUST NOT share +one EvidenceId. The same observation MUST NOT satisfy incompatible roles. +Strict TDD evidence is a RED → GREEN sequence, not a set of role labels: +GREEN-before-RED MUST fail closed, and RED-after-PATCH MUST fail closed. +Unique-sort of `verification.evidence_ids` MUST NOT be the assessment identity +and MUST NOT hide distinct role or obligation bindings. +(Previously: the same EvidenceId used as four roles was required to yield four passing assessments.) + +#### Scenario: Same EvidenceId as RED and GREEN fails closed - GIVEN one `evidence/v2` observation whose `evidence_id` is E -- AND that observation is bound as four distinct strategy roles to the same or different MUST obligations +- AND that observation is bound as both RED and GREEN strategy roles +- WHEN the verifier evaluates strategy evidence +- THEN verification MUST fail closed +- AND MUST NOT treat E as satisfying both roles + +#### Scenario: GREEN before RED fails closed + +- GIVEN Strict TDD or bug-strategy evidence whose GREEN observation precedes RED +- WHEN the verifier evaluates strategy evidence +- THEN verification MUST fail closed +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +#### Scenario: RED after PATCH fails closed + +- GIVEN Strict TDD or bug-strategy evidence whose RED observation occurs after PATCH +- WHEN the verifier evaluates strategy evidence +- THEN verification MUST fail closed +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +#### Scenario: Distinct tuples yield distinct assessment identities + +- GIVEN two persistable tuples that differ in `evidence_id`, `role`, or `obligation_id` +- AND neither tuple shares one EvidenceId across incompatible roles - WHEN assessments are persisted -- THEN exactly four distinct assessment identities MUST exist -- AND unique-sort of `verification.evidence_ids` MUST still list a single E without collapsing those assessments +- THEN their assessment identities MUST be distinct +- AND unique-sort of `verification.evidence_ids` MUST NOT collapse those assessments -### Requirement: Facade Fail-Closed On Required Projection {#REQ-independent-verification-007} +### Requirement:### Requirement: Facade Fail-Closed On Required Projection {#REQ-independent-verification-007} `verifyCandidate` MUST require a successful Assurance Graph projection before returning `ok: true`. If projection cannot materialize, the facade MUST fail @@ -226,3 +265,17 @@ graph, and MUST NOT emit `PASS` or `PASS WITH WARNINGS` in those cases. - THEN the facade MUST return `ok: false` with `GRAPH_PROJECTION_FAILED` - AND MUST omit `assurance_graph` - AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` + +### Requirement: Contract Digest Gate Before Strategy {#REQ-independent-verification-008} + +Before strategy evaluation, `input.contract.contract_digest` MUST equal +`executionGraph.contract_digest`. Mismatch MUST fail closed and MUST NOT +proceed to strategy evaluation. + +#### Scenario: Contract digest mismatch with Execution Graph fails closed before strategy + +- GIVEN a frozen Candidate whose `input.contract.contract_digest` is C2 +- AND an Execution Graph whose `contract_digest` is C1, where C1 ≠ C2 +- WHEN the verifier validates canonical bindings +- THEN verification MUST fail closed before strategy evaluation +- AND MUST NOT emit `PASS` or `PASS WITH WARNINGS` diff --git a/openspec/specs/kernel-contract-schemas/spec.md b/openspec/specs/kernel-contract-schemas/spec.md index 6129af8a..f27b823f 100644 --- a/openspec/specs/kernel-contract-schemas/spec.md +++ b/openspec/specs/kernel-contract-schemas/spec.md @@ -8,7 +8,7 @@ consumer tags — without executing the lifecycle reducer. ## Requirements -### Requirement: Versioned Schema Families With Id And Version {#REQ-kernel-contract-schemas-001} +### Requirement:### Requirement: Versioned Schema Families With Id And Version {#REQ-kernel-contract-schemas-001} The contract suite MUST publish a versioned JSON Schema for each family: state/transition, classification, contract, graph/node, work order/result, candidate, SourceSnapshot, WorkOrder, WorkResult, Candidate, evidence, verification, finding/review, failure/recovery, receipt, event, OperationPermit, OperationReceipt, effect-class, HostCapabilities, HostAdapter, ExecutionTransport, QuestionTransport, WorkerTransport, ToolExecutionTransport, DeliveryGateTransport, CapabilityProof, transport-request, transport-outcome, transport-failure, execution-graph, policy-snapshot, clarify-event, execution-budget, authority-effect-budget, causal-failure, failure-recovery-transition, workspace-descriptor, capsule-definition, work-result-execution-payload, containment-violation, assurance-graph, and assessment/binding. Every schema MUST declare a stable `$id` and an explicit version field (`schema_version` or equivalent). Consumers MUST be able to pin a schema by `$id`/version. (Previously: K6b closed the inventory at assurance-graph; this remediation adds an additive assessment/binding family without mutating evidence/v2, verification/v2, or K1 v1 pins.) @@ -84,7 +84,7 @@ The contract suite MUST publish a versioned JSON Schema for each family: state/t - THEN it MUST be present as a pinned versioned family with a distinct non-empty `$id` - AND evidence/v2, verification/v2, and K1 v1 pins MUST remain byte-identical -### Requirement: Valid And Invalid Fixtures Per Schema Family {#REQ-kernel-contract-schemas-002} +### Requirement:### Requirement: Valid And Invalid Fixtures Per Schema Family {#REQ-kernel-contract-schemas-002} Each schema family MUST ship at least one fixture that validates successfully and at least one fixture that MUST be rejected. Fixtures MUST exercise @@ -106,7 +106,7 @@ field, wrong type, or forbidden extra authority field as applicable). --- -### Requirement: Versioned Aliases Preserve Existing Tags {#REQ-kernel-contract-schemas-003} +### Requirement:### Requirement: Versioned Aliases Preserve Existing Tags {#REQ-kernel-contract-schemas-003} The suite MUST provide versioned aliases that map legacy or current stable codes/tags to the canonical vocabulary. Migration rules MUST preserve existing @@ -132,7 +132,7 @@ known consumer-facing tag without an explicit mapping entry. --- -### Requirement: Graph And Work-Order Shapes Are Consumable Contracts Only {#REQ-kernel-contract-schemas-004} +### Requirement:### Requirement: Graph And Work-Order Shapes Are Consumable Contracts Only {#REQ-kernel-contract-schemas-004} Graph/node and work-order/result schemas MUST be published as consumable contracts for later kernel work (K2–K4). Publishing those schemas MUST NOT @@ -149,7 +149,7 @@ reducer. --- -### Requirement: Schema Docs Cannot Name Unemitted Fields Or Commands {#REQ-kernel-contract-schemas-005} +### Requirement:### Requirement: Schema Docs Cannot Name Unemitted Fields Or Commands {#REQ-kernel-contract-schemas-005} Contract documentation and fixtures that claim to describe emitted surfaces MUST NOT name a field, operation, or command that the emitting code does not @@ -165,7 +165,7 @@ via `contract-lint` checkers). --- -### Requirement: Permit Receipt And Effect-Class Contract Families {#REQ-kernel-contract-schemas-006} +### Requirement:### Requirement: Permit Receipt And Effect-Class Contract Families {#REQ-kernel-contract-schemas-006} The contract suite MUST publish versioned JSON Schemas for `OperationPermit`, `OperationReceipt`, and effect-class (or effect-intent class) contracts. Each @@ -196,7 +196,7 @@ Candidate Evaluation Attestation or Delivery Authorization schemas. --- -### Requirement: Effect Class Enumeration Is Closed {#REQ-kernel-contract-schemas-007} +### Requirement:### Requirement: Effect Class Enumeration Is Closed {#REQ-kernel-contract-schemas-007} Effect-class contracts MUST accept only `pure | idempotent-keyed | probeable | compensatable | irreversible`. Any other @@ -211,7 +211,7 @@ value MUST fail schema validation. --- -### Requirement: Host And Capability-Proof Contract Families {#REQ-kernel-contract-schemas-008} +### Requirement:### Requirement: Host And Capability-Proof Contract Families {#REQ-kernel-contract-schemas-008} The contract suite MUST publish versioned JSON Schemas for `HostCapabilities`, `HostAdapter`, each of the five transport contracts, and `CapabilityProof`. @@ -244,7 +244,7 @@ Evaluation Attestation or Delivery Authorization schemas. --- -### Requirement: Capability State Enumeration Is Closed {#REQ-kernel-contract-schemas-009} +### Requirement:### Requirement: Capability State Enumeration Is Closed {#REQ-kernel-contract-schemas-009} Host capability-state fields MUST accept only `enforced | partial | instructional | unavailable`. Any other value MUST fail @@ -259,7 +259,7 @@ schema validation. --- -### Requirement: CapabilityProof Required Fields Are Closed {#REQ-kernel-contract-schemas-010} +### Requirement:### Requirement: CapabilityProof Required Fields Are Closed {#REQ-kernel-contract-schemas-010} CapabilityProof schemas MUST require `adapter_version`, `host_version`, `fixture`, and `evidence_digest` as non-empty fields. Omitting any required @@ -274,7 +274,7 @@ field MUST fail schema validation. --- -### Requirement: Transport Request Outcome And Failure Families {#REQ-kernel-contract-schemas-011} +### Requirement:### Requirement: Transport Request Outcome And Failure Families {#REQ-kernel-contract-schemas-011} The contract suite MUST publish additive versioned JSON Schema families `transport-request`, `transport-outcome`, and `transport-failure` at v1. Each @@ -320,7 +320,7 @@ and worker-fail. --- -### Requirement: Execution Identity Schemas With Non-Aliasing Fixtures {#REQ-kernel-contract-schemas-012} +### Requirement:### Requirement: Execution Identity Schemas With Non-Aliasing Fixtures {#REQ-kernel-contract-schemas-012} The contract suite MUST publish versioned JSON Schemas for `SourceSnapshot`, `WorkOrder`, `WorkResult`, and `Candidate` execution identity families. Each identity schema MUST declare a stable `$id` and explicit version field. The suite MUST define `candidate/v2.schema.json` (`$id: "ospec://schemas/kernel/candidate/v2"`) and `work-order/v2.schema.json` (`$id: "ospec://schemas/kernel/work-order/v2"`) with explicit `kind` field (`"candidate/v2"` and `"work-order/v2"`). WorkOrder v2 MUST require `source_snapshot_id` matching exactly `sha256:<64 lowercase hexadecimal characters>`; valid values MUST be preserved exactly by validation and consumers MUST NOT accept an absent, malformed, normalized, or substituted value. WorkOrder v2 `dependencies` array items MUST match pattern `^sha256:[a-f0-9]{64}$` representing canonical `WorkOrderId` sha256 digests of upstream prerequisite work orders. Candidate v2 MUST require `relation` and constrain it exactly to `exact`, `changed`, `ambiguous`, or `unknown`; it MUST allow nullable `predecessor_id` only as lineage metadata and MUST NOT retain aliases or retired relation values. `source-snapshot/v1.schema.json` and `work-result/v1.schema.json` MUST permit an optional `kind` property matching `"source-snapshot/v1"` and `"work-result/v1"` respectively without violating `additionalProperties: false`. Baseline `candidate/v1.schema.json`, `work-order/v1.schema.json`, and `K1_SCHEMA_BASELINE` MUST remain byte-identical immutable contracts. The suite MUST NOT retarget K1 pins to match altered v1 contents. Each identity family MUST include valid fixtures and negative non-aliasing fixtures demonstrating that `WorkResult` cannot validate as `Candidate`, and `Candidate` cannot validate as `CandidateEvaluationAttestation` or `DeliveryAuthorization`. Candidate fixtures MUST additionally prove rejection of retired relation vocabulary, impossible predecessor/relation combinations, commit projection, symlink changes, case-distinct paths, and projection changes. (Previously: WorkOrder v2 dependencies items were unconstrained strings without sha256 digest pattern enforcement.) @@ -396,7 +396,7 @@ The contract suite MUST publish versioned JSON Schemas for `SourceSnapshot`, `Wo --- -### Requirement: Canonical V2 Identity Schema Publication And Registry {#REQ-kernel-contract-schemas-013} +### Requirement:### Requirement: Canonical V2 Identity Schema Publication And Registry {#REQ-kernel-contract-schemas-013} Candidate v2 and WorkOrder v2 schemas MUST be published at filesystem paths `schemas/kernel/candidate/v2.schema.json` and `schemas/kernel/work-order/v2.schema.json` with `$id` values `ospec://schemas/kernel/candidate/v2` and `ospec://schemas/kernel/work-order/v2` respectively. Both schemas MUST be registered in `schemas/kernel/manifest.json` and `schemas/kernel/contract-claims.json`. Publication under wrong directory layouts `schemas/kernel/candidate-v2/` or `schemas/kernel/work-order-v2/` MUST NOT remain as the canonical publication; those paths MUST be removed or replaced by the canonical paths above. @@ -424,7 +424,7 @@ Candidate v2 and WorkOrder v2 schemas MUST be published at filesystem paths `sch --- -### Requirement: K1 Historical V1 Content And Pin Restore {#REQ-kernel-contract-schemas-014} +### Requirement:### Requirement: K1 Historical V1 Content And Pin Restore {#REQ-kernel-contract-schemas-014} Historical pre-K3 `candidate/v1` and `work-order/v1` schema file contents and their `K1_SCHEMA_BASELINE` pins MUST be restored from the `02e97a5` era. The system MUST restore file content and update pins to match those restored files. The system MUST NOT retarget `K1_SCHEMA_BASELINE` pins alone to match mutated post-`02e97a5` files while leaving drifted v1 content in place. Verification MUST NOT claim K1 pins intact when v1 schema file digests have drifted from the restored baseline. @@ -444,7 +444,7 @@ Historical pre-K3 `candidate/v1` and `work-order/v1` schema file contents and th --- -### Requirement: Execution Graph And Obligation Manifest Schema Family {#REQ-kernel-contract-schemas-015} +### Requirement:### Requirement: Execution Graph And Obligation Manifest Schema Family {#REQ-kernel-contract-schemas-015} The contract suite MUST publish `execution-graph/v1.schema.json` (`$id: "ospec://schemas/kernel/execution-graph/v1"`) with explicit `schema_version: 1`. The schema MUST require `schema_version`, `graph_id`, `contract_digest`, `policy_bundle_digest`, `policy_snapshot_id`, `source_snapshot_id`, `nodes` (array of semantic graph node objects), and `obligations` (array of obligation items). Both `source_snapshot_id` and `policy_snapshot_id` properties MUST match `^sha256:[a-f0-9]{64}$`. @@ -487,7 +487,7 @@ Each obligation item MUST require `id`, `criticality` (`must | should | may`), ` --- -### Requirement: PolicySnapshot Schema Family With Effective Rules {#REQ-kernel-contract-schemas-016} +### Requirement:### Requirement: PolicySnapshot Schema Family With Effective Rules {#REQ-kernel-contract-schemas-016} The contract suite MUST publish `policy-snapshot/v1.schema.json` (`$id: "ospec://schemas/kernel/policy-snapshot/v1"`) with explicit `schema_version: 1`. The schema MUST require `snapshot_id`, `policy_bundle_digest`, `compiler_version`, `classifier_version`, `runtime_version`, and `effective_rules` (array of resolved rule strings or objects). The schema MUST enforce `additionalProperties: false` and MUST NOT alias CandidateEvaluationAttestation or DeliveryAuthorization schemas. The family MUST ship valid and invalid fixtures demonstrating valid structure and rejection of malformed or missing fields. @@ -507,7 +507,7 @@ The contract suite MUST publish `policy-snapshot/v1.schema.json` (`$id: "ospec:/ --- -### Requirement: ClarifyEvent Schema Family {#REQ-kernel-contract-schemas-017} +### Requirement:### Requirement: ClarifyEvent Schema Family {#REQ-kernel-contract-schemas-017} The contract suite MUST publish `clarify-event/v1.schema.json` (`$id: "ospec://schemas/kernel/clarify-event/v1"`) with explicit `schema_version: 1`. The schema MUST require `event_id`, `question_id`, `answer`, `timestamp`, and `affected_nodes` (array of string node IDs). The schema MUST enforce `additionalProperties: false` and strict non-aliasing against general event or transport schemas. The family MUST include valid fixtures and invalid fixtures demonstrating rejection of missing required fields. @@ -527,7 +527,7 @@ The contract suite MUST publish `clarify-event/v1.schema.json` (`$id: "ospec://s --- -### Requirement: PolicySnapshot v1 Canonical Binding Validation {#REQ-kernel-contract-schemas-018} +### Requirement:### Requirement: PolicySnapshot v1 Canonical Binding Validation {#REQ-kernel-contract-schemas-018} The contract suite MUST provide a canonical validation function `validatePolicySnapshotBinding(snapshot)` that validates `PolicySnapshot` records against `policy-snapshot/v1.schema.json` and cryptographically verifies that `snapshot.snapshot_id === computePolicySnapshotDigest(snapshot)`. @@ -566,7 +566,7 @@ If `snapshot` is null, non-object, fails schema validation, has a malformed dige --- -### Requirement: Execution Budget And Authority Effect Budget Schema Families {#REQ-kernel-contract-schemas-019} +### Requirement:### Requirement: Execution Budget And Authority Effect Budget Schema Families {#REQ-kernel-contract-schemas-019} The contract suite MUST publish `execution-budget/v1.schema.json` (`$id: "ospec://schemas/kernel/execution-budget/v1"`) and `authority-effect-budget/v1.schema.json` (`$id: "ospec://schemas/kernel/authority-effect-budget/v1"`) with explicit `schema_version: 1`. @@ -592,7 +592,7 @@ Both schemas MUST enforce `additionalProperties: false`. The family MUST ship va --- -### Requirement: Causal Failure And Recovery Transition Schema Families {#REQ-kernel-contract-schemas-020} +### Requirement:### Requirement: Causal Failure And Recovery Transition Schema Families {#REQ-kernel-contract-schemas-020} The contract suite MUST publish `causal-failure/v1.schema.json` (`$id: "ospec://schemas/kernel/causal-failure/v1"`) and `failure-recovery-transition/v1.schema.json` (`$id: "ospec://schemas/kernel/failure-recovery-transition/v1"`) with explicit `schema_version: 1`. @@ -618,7 +618,7 @@ Both schemas MUST enforce `additionalProperties: false` and ship valid and inval --- -### Requirement: Workspace Descriptor And Capsule Definition Schema Families {#REQ-kernel-contract-schemas-021} +### Requirement:### Requirement: Workspace Descriptor And Capsule Definition Schema Families {#REQ-kernel-contract-schemas-021} The contract suite MUST publish `workspace-descriptor/v1.schema.json` (`$id: "ospec://schemas/kernel/workspace-descriptor/v1"`) and `capsule-definition/v1.schema.json` (`$id: "ospec://schemas/kernel/capsule-definition/v1"`) with explicit `schema_version: 1`. @@ -651,7 +651,7 @@ Both schemas MUST enforce `additionalProperties: false` and ship valid and inval --- -### Requirement: Work Result Execution Payload And Containment Violation Schema Families {#REQ-kernel-contract-schemas-022} +### Requirement:### Requirement: Work Result Execution Payload And Containment Violation Schema Families {#REQ-kernel-contract-schemas-022} The contract suite MUST publish `work-result-execution-payload/v1.schema.json` (`$id: "ospec://schemas/kernel/work-result-execution-payload/v1"`) and `containment-violation/v1.schema.json` (`$id: "ospec://schemas/kernel/containment-violation/v1"`) with explicit `schema_version: 1`. @@ -687,7 +687,7 @@ Both schemas MUST enforce `additionalProperties: false`. Negative non-aliasing f --- -### Requirement: Work Order V2 Requires Closed Capsule Inputs {#REQ-kernel-contract-schemas-023} +### Requirement:### Requirement: Work Order V2 Requires Closed Capsule Inputs {#REQ-kernel-contract-schemas-023} `work-order/v2.schema.json` MUST add `capsule_inputs` as a required property: an array of one or more non-empty relative file-path strings. Each item MUST be a concrete relative path (`minLength: 1`), MUST NOT match glob metacharacters (`*`, `?`, `[`), MUST NOT contain `..`, and MUST NOT be absolute. `additionalProperties` MUST remain `false`. `work-order/v1.schema.json` and `K1_SCHEMA_BASELINE` MUST remain byte-identical. @@ -717,7 +717,7 @@ Valid v2 fixtures MUST include `capsule_inputs`. Negative fixtures MUST reject: - WHEN verified after the v2 `capsule_inputs` addition - THEN v1 schema bytes and K1 pins MUST remain byte-identical to the frozen baseline -### Requirement: Evidence V2 Provenance Binding Distinct From Verdict {#REQ-kernel-contract-schemas-024} +### Requirement:### Requirement: Evidence V2 Provenance Binding Distinct From Verdict {#REQ-kernel-contract-schemas-024} The suite MUST publish `evidence/v2.schema.json` (`$id: "ospec://schemas/kernel/evidence/v2"`) with `schema_version: 2` and @@ -750,7 +750,7 @@ payloads that include `verdict`. - WHEN verified after evidence/v2 publication - THEN v1 schema and fixture bytes and K1 pins MUST remain byte-identical -### Requirement: Verification V2 Verdict Distinct From Evidence {#REQ-kernel-contract-schemas-025} +### Requirement:### Requirement: Verification V2 Verdict Distinct From Evidence {#REQ-kernel-contract-schemas-025} The suite MUST publish `verification/v2.schema.json` (`$id: "ospec://schemas/kernel/verification/v2"`) with `schema_version: 2` and @@ -780,7 +780,7 @@ verification/v2 as evidence/v2 and evidence/v2 as verification/v2. - WHEN verified after verification/v2 publication - THEN v1 schema and fixture bytes and K1 pins MUST remain byte-identical -### Requirement: Assurance Graph Schema Family And Equivalence Manifest {#REQ-kernel-contract-schemas-026} +### Requirement:### Requirement: Assurance Graph Schema Family And Equivalence Manifest {#REQ-kernel-contract-schemas-026} The suite MUST publish `assurance-graph/v1.schema.json` (`$id: "ospec://schemas/kernel/assurance-graph/v1"`) with `schema_version: 1`. @@ -812,18 +812,21 @@ digests. The suite MUST publish an additive assessment/binding schema family with a distinct `$id` and explicit `schema_version`. Exact `$id` is design-owned. Required persistable fields: assessment identity, `evidence_id`, `role`, -`obligation_id`, `node_id`, `candidate_id` (`^sha256:[a-f0-9]{64}$`), and bound -policy-snapshot identity. Assessment identity MUST incorporate `role` and -`obligation_id`. The schema MUST enforce `additionalProperties: false` and -MUST NOT include `verdict`. The family MUST NOT validate as `evidence/v2` or -`verification/v2`. `evidence/v2`, `verification/v2`, and K1 v1 schema bytes -and `K1_SCHEMA_BASELINE` pins MUST remain byte-identical. Valid and invalid -fixtures MUST cover a complete binding, missing required fields, and -cross-family substitution. +`obligation_id`, `node_id`, `candidate_id` (`^sha256:[a-f0-9]{64}$`), bound +policy-snapshot identity, and an additive coverage field recording the +satisfied `required_evidence` tokens (exact field name design-owned). +Assessment identity MUST incorporate `role` and `obligation_id`. The schema +MUST enforce `additionalProperties: false` and MUST NOT include `verdict`. +The family MUST NOT validate as `evidence/v2` or `verification/v2`. +`evidence/v2`, `verification/v2`, and K1 v1 schema bytes and +`K1_SCHEMA_BASELINE` pins MUST remain byte-identical. Valid and invalid +fixtures MUST cover a complete binding including coverage, missing required +fields, omitted coverage, and cross-family substitution. +(Previously: assessment/binding required identity fields but no persistable coverage of satisfied required_evidence tokens.) #### Scenario: Valid assessment fixture passes -- GIVEN a complete assessment/binding payload with role, obligation_id, node_id, evidence_id, and policy-snapshot identity +- GIVEN a complete assessment/binding payload with role, obligation_id, node_id, evidence_id, policy-snapshot identity, and the additive coverage field - WHEN validated against the assessment/binding schema - THEN validation MUST succeed @@ -840,8 +843,14 @@ cross-family substitution. - THEN all four MUST be schema-valid - AND their assessment identities MUST be pairwise distinct +#### Scenario: Assessment fixture without coverage field fails closed + +- GIVEN an assessment/binding payload that omits the additive coverage field +- WHEN validated against the assessment/binding schema +- THEN validation MUST fail closed identifying the missing coverage field + #### Scenario: Evidence v2, verification v2, and K1 v1 pins remain frozen - GIVEN `evidence/v2.schema.json`, `verification/v2.schema.json`, K1 v1 schemas, and `K1_SCHEMA_BASELINE` -- WHEN verified after assessment/binding publication +- WHEN verified after the additive coverage field is present on assessment/binding - THEN those schema and fixture bytes and K1 pins MUST remain byte-identical diff --git a/package.json b/package.json index cfae6270..ab8b5d0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ospec-workflow", - "version": "2.51.0", + "version": "2.52.0", "private": true, "description": "Spec-Driven Development workflow with OpenSpec, strict TDD, phase agents, skills, hooks, and verification contracts.", "license": "MIT", diff --git a/schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json b/schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json new file mode 100644 index 00000000..220455c6 --- /dev/null +++ b/schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json @@ -0,0 +1,11 @@ +{ + "schema_version": 1, + "kind": "assessment/v1", + "assessment_id": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "evidence_id": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "role": "acceptance", + "obligation_id": "req-repair-001", + "node_id": "repair-core", + "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" +} diff --git a/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json b/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json index 29caa795..db96f870 100644 --- a/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json +++ b/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json @@ -8,5 +8,6 @@ "node_id": "repair-core", "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "evidence_requirements_satisfied": ["ev:test-pass"], "verdict": "PASS" } diff --git a/schemas/kernel/assessment/fixtures/valid/v1-complete.json b/schemas/kernel/assessment/fixtures/valid/v1-complete.json index 220455c6..c7a494b6 100644 --- a/schemas/kernel/assessment/fixtures/valid/v1-complete.json +++ b/schemas/kernel/assessment/fixtures/valid/v1-complete.json @@ -7,5 +7,6 @@ "obligation_id": "req-repair-001", "node_id": "repair-core", "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "evidence_requirements_satisfied": ["ev:test-pass"] } diff --git a/schemas/kernel/assessment/fixtures/valid/v1-four-roles.json b/schemas/kernel/assessment/fixtures/valid/v1-four-roles.json index 66c16f71..bc428d3a 100644 --- a/schemas/kernel/assessment/fixtures/valid/v1-four-roles.json +++ b/schemas/kernel/assessment/fixtures/valid/v1-four-roles.json @@ -8,7 +8,8 @@ "obligation_id": "req-repair-001", "node_id": "repair-core", "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "evidence_requirements_satisfied": ["ev:test-pass"] }, { "schema_version": 1, @@ -19,7 +20,8 @@ "obligation_id": "req-repair-001", "node_id": "repair-core", "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "evidence_requirements_satisfied": ["ev:test-pass"] }, { "schema_version": 1, @@ -30,7 +32,8 @@ "obligation_id": "req-repair-001", "node_id": "repair-core", "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "evidence_requirements_satisfied": ["ev:test-pass"] }, { "schema_version": 1, @@ -41,6 +44,7 @@ "obligation_id": "req-repair-001", "node_id": "repair-core", "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "evidence_requirements_satisfied": ["ev:test-pass"] } ] diff --git a/schemas/kernel/assessment/v1.schema.json b/schemas/kernel/assessment/v1.schema.json index 831bfdf0..a97ac5da 100644 --- a/schemas/kernel/assessment/v1.schema.json +++ b/schemas/kernel/assessment/v1.schema.json @@ -14,7 +14,8 @@ "obligation_id", "node_id", "candidate_id", - "policy_snapshot_id" + "policy_snapshot_id", + "evidence_requirements_satisfied" ], "properties": { "schema_version": { @@ -52,6 +53,14 @@ "policy_snapshot_id": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" + }, + "evidence_requirements_satisfied": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true } }, "additionalProperties": false diff --git a/schemas/kernel/contract-claims.json b/schemas/kernel/contract-claims.json index 31d2ecc9..076f2e94 100644 --- a/schemas/kernel/contract-claims.json +++ b/schemas/kernel/contract-claims.json @@ -689,7 +689,8 @@ "obligation_id", "node_id", "candidate_id", - "policy_snapshot_id" + "policy_snapshot_id", + "evidence_requirements_satisfied" ], "enum_values": {}, "command_shapes": [] diff --git a/scripts/k6b-verifier-assurance-graph-e2e.test.js b/scripts/k6b-verifier-assurance-graph-e2e.test.js index 5af01922..267c2077 100644 --- a/scripts/k6b-verifier-assurance-graph-e2e.test.js +++ b/scripts/k6b-verifier-assurance-graph-e2e.test.js @@ -44,10 +44,10 @@ const OBLIGATIONS = [ function featureEvidence() { return [ - { role: "acceptance", bytes: "acceptance ok", provenance: "runtime-observed", origin: "e2e-acceptance", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, - { role: "invariants", bytes: "invariants ok", provenance: "runtime-observed", origin: "e2e-invariants", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, - { role: "integration", bytes: "integration ok", provenance: "runtime-observed", origin: "e2e-integration", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, - { role: "negative", bytes: "negative ok", provenance: "runtime-observed", origin: "e2e-negative", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, + { role: "acceptance", bytes: "acceptance ok", provenance: "runtime-observed", origin: "e2e-acceptance", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, + { role: "invariants", bytes: "invariants ok", provenance: "runtime-observed", origin: "e2e-invariants", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, + { role: "integration", bytes: "integration ok", provenance: "runtime-observed", origin: "e2e-integration", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, + { role: "negative", bytes: "negative ok", provenance: "runtime-observed", origin: "e2e-negative", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, ]; } diff --git a/scripts/lib/assurance-graph/index.js b/scripts/lib/assurance-graph/index.js index 400d3755..6af4c600 100644 --- a/scripts/lib/assurance-graph/index.js +++ b/scripts/lib/assurance-graph/index.js @@ -1,7 +1,8 @@ "use strict"; -const { projectAssuranceGraph, rejectForbidden } = require("./projector.js"); +const { projectAssuranceGraph, rejectForbidden, canonicalize, computeGraphId } = require("./projector.js"); const { computeInvalidationClosure, isEvidenceTransitivelyInvalidated } = require("./invalidation.js"); +const { validateAssessment } = require("../independent-verifier/assessment.js"); function fail(reason_code, error) { return { ok: false, reason_code, error: error || reason_code }; @@ -17,19 +18,82 @@ function fail(reason_code, error) { function reconcileAssuranceGraph(stored, canonicalInput) { const projected = projectAssuranceGraph(canonicalInput); if (!projected.ok) return projected; - if (!stored || typeof stored !== "object" || stored.graph_id !== projected.graph.graph_id) { + if (!stored || typeof stored !== "object") { return fail("GRAPH_DIVERGENCE", "stored Assurance Graph does not recompute from canonical inputs"); } - const storedEdges = JSON.stringify((stored.edges || []).map((e) => [e.from, e.relation, e.to]).sort()); - const projectedEdges = JSON.stringify( - projected.graph.edges.map((e) => [e.from, e.relation, e.to]).sort() - ); - if (storedEdges !== projectedEdges) { - return fail("GRAPH_DIVERGENCE", "stored edges diverge from canonical projection"); + if (stored.kind !== "assurance-graph/v1" || stored.schema_version !== 1) { + return fail("GRAPH_DIVERGENCE", "stored graph kind or schema_version diverges"); + } + if (!Array.isArray(stored.nodes) || !Array.isArray(stored.edges) || !stored.canonical_inputs) { + return fail("GRAPH_DIVERGENCE", "stored graph payload is incomplete"); + } + const canonicalStored = canonicalize(stored.nodes, stored.edges); + const recomputedStoredId = computeGraphId({ + candidate_id: stored.candidate_id, + canonical_inputs: stored.canonical_inputs, + nodes: canonicalStored.nodes, + edges: canonicalStored.edges, + }); + if (stored.graph_id !== recomputedStoredId) { + return fail("GRAPH_DIVERGENCE", "stored graph_id does not match its stored payload"); + } + const fields = ["schema_version", "kind", "candidate_id", "graph_id", "nodes", "edges", "canonical_inputs"]; + for (const field of fields) { + if (JSON.stringify(stored[field]) !== JSON.stringify(projected.graph[field])) { + return fail("GRAPH_DIVERGENCE", `stored ${field} diverges from canonical projection`); + } } return { ok: true, graph: projected.graph }; } +function isApprovedDeferred(obligation) { + return Boolean( + obligation && obligation.deferred && typeof obligation.deferred.reason === "string" && obligation.deferred.reason.trim() && + typeof obligation.deferred.approved_by === "string" && obligation.deferred.approved_by.trim() + ); +} + +function validateReplayAssessments(persistable) { + const assessments = Array.isArray(persistable.assessments) ? persistable.assessments : []; + const evidence = Array.isArray(persistable.evidence) ? persistable.evidence : []; + const graph = persistable.executionGraph; + const candidate = persistable.candidate; + if (!graph || !candidate || !Array.isArray(graph.obligations) || !Array.isArray(graph.nodes)) { + return fail("GRAPH_DIVERGENCE", "persistable graph, candidate, nodes, and obligations are required for replay"); + } + const evidenceById = new Map(evidence.map((record) => [record && record.evidence_id, record])); + const obligations = new Map(graph.obligations.map((obligation) => [obligation && obligation.id, obligation])); + const coveredByObligation = new Map(); + for (const assessment of assessments) { + const valid = validateAssessment(assessment); + if (!valid.ok) return fail("GRAPH_DIVERGENCE", valid.error); + const record = valid.assessment; + const obligation = obligations.get(record.obligation_id); + const evidenceRecord = evidenceById.get(record.evidence_id); + if (!obligation || !evidenceRecord || record.candidate_id !== candidate.candidate_id || + record.policy_snapshot_id !== graph.policy_snapshot_id || record.node_id !== evidenceRecord.node_id || + !Array.isArray(obligation.implemented_by) || !obligation.implemented_by.includes(record.node_id)) { + return fail("GRAPH_DIVERGENCE", "persisted assessment binding diverges from evidence, Candidate, policy, or obligation"); + } + const required = new Set(Array.isArray(obligation.required_evidence) ? obligation.required_evidence : []); + if (record.evidence_requirements_satisfied.some((token) => !required.has(token))) { + return fail("GRAPH_DIVERGENCE", "assessment coverage contains a token outside the obligation requirement"); + } + const coverage = coveredByObligation.get(record.obligation_id) || new Set(); + for (const token of record.evidence_requirements_satisfied) coverage.add(token); + coveredByObligation.set(record.obligation_id, coverage); + } + for (const obligation of graph.obligations) { + if (!obligation || String(obligation.criticality || "must").toLowerCase() !== "must" || isApprovedDeferred(obligation)) continue; + const required = Array.isArray(obligation.required_evidence) ? obligation.required_evidence : []; + const covered = coveredByObligation.get(obligation.id) || new Set(); + if (required.length === 0 || required.some((token) => !covered.has(token))) { + return fail("GRAPH_DIVERGENCE", `persisted assessments do not satisfy MUST obligation ${obligation.id}`); + } + } + return { ok: true }; +} + /** * Replay a projection from persistable assessments, evidence, verification, and canonical_inputs. * Never consumes ephemeral projector obligation_ids. @@ -38,6 +102,8 @@ function reconcileAssuranceGraph(stored, canonicalInput) { * @returns {{ ok: true, graph: object } | { ok: false, reason_code: string }} */ function replayAssuranceGraph(persistable = {}) { + const assessmentValidation = validateReplayAssessments(persistable); + if (!assessmentValidation.ok) return assessmentValidation; return projectAssuranceGraph({ canonicalInputs: persistable.canonical_inputs || persistable.canonicalInputs, candidate: persistable.candidate, @@ -76,6 +142,7 @@ module.exports = { projectAssuranceGraph, reconcileAssuranceGraph, replayAssuranceGraph, + validateReplayAssessments, rejectForbidden, computeInvalidationClosure, isEvidenceTransitivelyInvalidated, diff --git a/scripts/lib/assurance-graph/index.test.js b/scripts/lib/assurance-graph/index.test.js index b31bc70c..77a0e1aa 100644 --- a/scripts/lib/assurance-graph/index.test.js +++ b/scripts/lib/assurance-graph/index.test.js @@ -17,6 +17,8 @@ const { isEvidenceTransitivelyInvalidated, } = require("./index.js"); const { verifyCandidate } = require("../independent-verifier/index.js"); +const { computeAssessmentId } = require("../independent-verifier/assessment.js"); +const { canonicalize, computeGraphId } = require("./projector.js"); const SAMPLE_NODES = [ { @@ -76,10 +78,10 @@ function compileGraph() { function featureRaw() { return [ - { role: "acceptance", bytes: "acceptance", provenance: "runtime-observed", origin: "a", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, - { role: "invariants", bytes: "invariants", provenance: "runtime-observed", origin: "i", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, - { role: "contract", bytes: "contract", provenance: "runtime-observed", origin: "c", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, - { role: "negative", bytes: "negative", provenance: "runtime-observed", origin: "n", node_id: "repair-core", obligation_ids: ["req-repair-001"] }, + { role: "acceptance", bytes: "acceptance", provenance: "runtime-observed", origin: "a", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, + { role: "invariants", bytes: "invariants", provenance: "runtime-observed", origin: "i", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, + { role: "contract", bytes: "contract", provenance: "runtime-observed", origin: "c", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, + { role: "negative", bytes: "negative", provenance: "runtime-observed", origin: "n", node_id: "repair-core", obligation_ids: ["req-repair-001"], evidence_requirements_satisfied: ["ev:test-pass"] }, ]; } @@ -92,6 +94,7 @@ function verifiedProjection() { const verified = verifyCandidate({ candidate, executionGraph, + contract: { contract_digest: executionGraph.contract_digest }, repository: { files }, declaredStrategy: "feature", collector: HARNESS_COLLECTOR, @@ -101,6 +104,26 @@ function verifiedProjection() { return { files, candidate, executionGraph, verified }; } +function withAssessmentFields(assessment, fields) { + const updated = { ...assessment, ...fields }; + return { ...updated, assessment_id: computeAssessmentId(updated) }; +} + +function withStoredGraphId(stored) { + const canonical = canonicalize(stored.nodes, stored.edges); + return { + ...stored, + nodes: canonical.nodes, + edges: canonical.edges, + graph_id: computeGraphId({ + candidate_id: stored.candidate_id, + canonical_inputs: stored.canonical_inputs, + nodes: canonical.nodes, + edges: canonical.edges, + }), + }; +} + test("REQ-assurance-graph-002: same inputs yield the same digest and edges despite permutation", () => { const { candidate, executionGraph, verified } = verifiedProjection(); const classified = verified.evidence.map((evidence, index) => ({ @@ -194,7 +217,7 @@ test("REQ-assurance-graph-001: matching canonical inputs project; divergence fai ); assert.equal(edgeDiverged.ok, false); assert.equal(edgeDiverged.reason_code, "GRAPH_DIVERGENCE"); - assert.match(edgeDiverged.error, /stored edges diverge from canonical projection/); + assert.match(edgeDiverged.error, /stored graph_id does not match its stored payload/); }); test("REQ-harness-authority-canon-010: APIs return new objects without write-through", () => { @@ -303,7 +326,7 @@ test("REQ-harness-authority-canon-010: graph used as approval or delivery author assert.equal(result.reason_code, "GRAPH_AUTHORITY_MISUSE"); }); -test("REQ-assurance-graph-002: graph_id changes when canonical inputs change; permutation does not", () => { +test("REQ-assurance-graph-007: contradictory canonical inputs fail closed; permutation does not", () => { const { candidate, executionGraph, verified } = verifiedProjection(); const baseInput = { candidate, @@ -323,8 +346,8 @@ test("REQ-assurance-graph-002: graph_id changes when canonical inputs change; pe contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, }); - assert.equal(flippedContract.ok, true); - assert.notEqual(base.graph.graph_id, flippedContract.graph.graph_id); + assert.equal(flippedContract.ok, false); + assert.equal(flippedContract.reason_code, "GRAPH_DIVERGENCE"); const flippedPolicy = projectAssuranceGraph({ ...baseInput, @@ -333,7 +356,8 @@ test("REQ-assurance-graph-002: graph_id changes when canonical inputs change; pe policy_snapshot_id: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", }, }); - assert.notEqual(base.graph.graph_id, flippedPolicy.graph.graph_id); + assert.equal(flippedPolicy.ok, false); + assert.equal(flippedPolicy.reason_code, "GRAPH_DIVERGENCE"); const flippedExec = projectAssuranceGraph({ ...baseInput, @@ -342,7 +366,8 @@ test("REQ-assurance-graph-002: graph_id changes when canonical inputs change; pe execution_graph_digest: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", }, }); - assert.notEqual(base.graph.graph_id, flippedExec.graph.graph_id); + assert.equal(flippedExec.ok, false); + assert.equal(flippedExec.reason_code, "GRAPH_DIVERGENCE"); const flippedOpenspec = projectAssuranceGraph({ ...baseInput, @@ -361,6 +386,15 @@ test("REQ-assurance-graph-002: graph_id changes when canonical inputs change; pe assert.equal(base.graph.graph_id, permutedNodes.graph.graph_id); }); +test("REQ-assurance-graph-007: missing required canonical digest is never fingerprinted", () => { + const { candidate, executionGraph } = verifiedProjection(); + const missingPolicy = { ...executionGraph }; + delete missingPolicy.policy_snapshot_id; + const result = projectAssuranceGraph({ candidate, executionGraph: missingPolicy }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "GRAPH_DIVERGENCE"); +}); + test("REQ-assurance-graph-005: rejectForbidden matches kind/namespace, not id substring", () => { const allowed = rejectForbidden( [{ id: "REQ-add-authorization-header", kind: "requirement" }], @@ -412,7 +446,8 @@ test("REQ-assurance-graph-006: replay from persistable outputs is byte-identical contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, }); - assert.notEqual(churned.graph.graph_id, verified.assurance_graph.graph_id); + assert.equal(churned.ok, false); + assert.equal(churned.reason_code, "GRAPH_DIVERGENCE"); const diverged = reconcileAssuranceGraph(verified.assurance_graph, { candidate, executionGraph, @@ -428,3 +463,154 @@ test("REQ-assurance-graph-006: replay from persistable outputs is byte-identical assert.equal(diverged.reason_code, "GRAPH_DIVERGENCE"); }); +test("REQ-assurance-graph-006/008: replay and reconcile reject assessment and stored-payload tampering", () => { + const { candidate, executionGraph, verified } = verifiedProjection(); + const persistable = { + candidate, + executionGraph, + evidence: verified.evidence, + assessments: verified.assessments, + verification: verified.verification, + canonical_inputs: verified.assurance_graph.canonical_inputs, + }; + const tamperedAssessment = replayAssuranceGraph({ + ...persistable, + assessments: [{ ...verified.assessments[0], assessment_id: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" }, ...verified.assessments.slice(1)], + }); + assert.equal(tamperedAssessment.ok, false); + assert.equal(tamperedAssessment.reason_code, "GRAPH_DIVERGENCE"); + + const tamperedNode = reconcileAssuranceGraph( + { ...verified.assurance_graph, nodes: [...verified.assurance_graph.nodes, { id: "extra", kind: "source" }] }, + { candidate, executionGraph, evidence: verified.evidence, assessments: verified.assessments, verification: verified.verification } + ); + assert.equal(tamperedNode.ok, false); + assert.equal(tamperedNode.reason_code, "GRAPH_DIVERGENCE"); +}); + +test("REQ-assurance-graph-006: replay rejects every persisted assessment binding mutation", () => { + const { candidate, executionGraph, verified } = verifiedProjection(); + const persistable = { + candidate, + executionGraph, + evidence: verified.evidence, + assessments: verified.assessments, + verification: verified.verification, + canonical_inputs: verified.assurance_graph.canonical_inputs, + }; + const assessment = verified.assessments[0]; + const missingEvidenceId = "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + + const cases = [ + { + name: "malformed assessment schema", + input: { + ...persistable, + assessments: [{ ...assessment, kind: "evidence/v2" }, ...verified.assessments.slice(1)], + }, + }, + { + name: "coverage outside the obligation requirements", + input: { + ...persistable, + assessments: [withAssessmentFields(assessment, { evidence_requirements_satisfied: ["ev:unexpected"] }), ...verified.assessments.slice(1)], + }, + }, + { + name: "candidate mismatch", + input: { + ...persistable, + assessments: [withAssessmentFields(assessment, { candidate_id: missingEvidenceId }), ...verified.assessments.slice(1)], + }, + }, + { + name: "policy mismatch", + input: { + ...persistable, + assessments: [withAssessmentFields(assessment, { policy_snapshot_id: missingEvidenceId }), ...verified.assessments.slice(1)], + }, + }, + { + name: "missing evidence", + input: { + ...persistable, + assessments: [withAssessmentFields(assessment, { evidence_id: missingEvidenceId }), ...verified.assessments.slice(1)], + }, + }, + { + name: "unknown obligation", + input: { + ...persistable, + assessments: [withAssessmentFields(assessment, { obligation_id: "req-unknown-001" }), ...verified.assessments.slice(1)], + }, + }, + { + name: "non-implementing node", + input: { + ...persistable, + evidence: [{ ...verified.evidence[0], node_id: "non-implementing-node" }, ...verified.evidence.slice(1)], + assessments: [withAssessmentFields(assessment, { node_id: "non-implementing-node" }), ...verified.assessments.slice(1)], + }, + }, + { + name: "node_id mismatch", + input: { + ...persistable, + evidence: [{ ...verified.evidence[0], node_id: "evidence-node-mismatch" }, ...verified.evidence.slice(1)], + }, + }, + ]; + + for (const { name, input } of cases) { + const replayed = replayAssuranceGraph(input); + assert.equal(replayed.ok, false, name); + assert.equal(replayed.reason_code, "GRAPH_DIVERGENCE", name); + } +}); + +test("REQ-assurance-graph-008: reconcile rejects stored identity mutations after recomputing stored graph_id", () => { + const { candidate, executionGraph, verified } = verifiedProjection(); + const canonicalInput = { + candidate, + executionGraph, + evidence: verified.evidence, + assessments: verified.assessments, + verification: verified.verification, + }; + const stored = verified.assurance_graph; + + const cases = [ + { + name: "canonical_inputs", + graph: withStoredGraphId({ + ...stored, + canonical_inputs: { + ...stored.canonical_inputs, + openspec_input_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + }), + }, + { + name: "candidate_id", + graph: withStoredGraphId({ + ...stored, + candidate_id: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }), + }, + { + name: "kind", + graph: withStoredGraphId({ ...stored, kind: "assurance-graph/v2" }), + }, + { + name: "schema_version", + graph: withStoredGraphId({ ...stored, schema_version: 2 }), + }, + ]; + + for (const { name, graph } of cases) { + const reconciled = reconcileAssuranceGraph(graph, canonicalInput); + assert.equal(reconciled.ok, false, name); + assert.equal(reconciled.reason_code, "GRAPH_DIVERGENCE", name); + } +}); + diff --git a/scripts/lib/assurance-graph/projector.js b/scripts/lib/assurance-graph/projector.js index 40ed2406..087918fa 100644 --- a/scripts/lib/assurance-graph/projector.js +++ b/scripts/lib/assurance-graph/projector.js @@ -102,6 +102,18 @@ function pushEdge(edges, from, relation, to) { edges.push({ from, relation, to }); } +function computeGraphId(payload) { + return sha256Fingerprint("assurance-graph/v1", { + candidate_id: payload.candidate_id, + contract_digest: payload.canonical_inputs.contract_digest, + policy_snapshot_id: payload.canonical_inputs.policy_snapshot_id, + execution_graph_digest: payload.canonical_inputs.execution_graph_digest, + openspec_input_digest: payload.canonical_inputs.openspec_input_digest, + nodes: payload.nodes, + edges: payload.edges, + }); +} + function resolveCanonicalInputDigests(input) { const provided = input.canonicalInputs && typeof input.canonicalInputs === "object" ? input.canonicalInputs : {}; const graph = input.executionGraph || {}; @@ -117,22 +129,28 @@ function resolveCanonicalInputDigests(input) { source_snapshot_id: graph.source_snapshot_id || (provided.sourceSnapshot && provided.sourceSnapshot.source_snapshot_id) || null, }); - return { + const digests = { contract_digest: contractDigest, policy_snapshot_id: policySnapshotId, execution_graph_digest: executionGraphDigest, openspec_input_digest: openspecInputDigest, }; -} - -function persistableCanonicalInputs(digests) { - const persistable = {}; - for (const key of ["contract_digest", "policy_snapshot_id", "execution_graph_digest", "openspec_input_digest"]) { - if (typeof digests[key] === "string" && SHA256.test(digests[key])) { - persistable[key] = digests[key]; + for (const [key, value] of Object.entries(digests)) { + if (typeof value !== "string" || !SHA256.test(value)) { + return fail("GRAPH_DIVERGENCE", `canonical input ${key} must be a resolved sha256 digest`); } } - return persistable; + const suppliedContract = provided.contract_digest || contract.contract_digest; + if (suppliedContract && suppliedContract !== graph.contract_digest) { + return fail("GRAPH_DIVERGENCE", "canonical contract digest contradicts Execution Graph"); + } + if (provided.policy_snapshot_id && provided.policy_snapshot_id !== graph.policy_snapshot_id) { + return fail("GRAPH_DIVERGENCE", "canonical policy snapshot contradicts Execution Graph"); + } + if (provided.execution_graph_digest && provided.execution_graph_digest !== graph.graph_id) { + return fail("GRAPH_DIVERGENCE", "canonical execution graph digest contradicts Execution Graph"); + } + return { ok: true, canonical_inputs: digests }; } /** @@ -208,13 +226,12 @@ function projectAssuranceGraph(input = {}) { if (!forbidden.ok) return forbidden; const canonical = canonicalize(nodes, edges); - const canonicalInputs = resolveCanonicalInputDigests(input); - const graphId = sha256Fingerprint("assurance-graph/v1", { + const resolvedInputs = resolveCanonicalInputDigests(input); + if (!resolvedInputs.ok) return resolvedInputs; + const canonicalInputs = resolvedInputs.canonical_inputs; + const graphId = computeGraphId({ candidate_id: candidateId, - contract_digest: canonicalInputs.contract_digest, - policy_snapshot_id: canonicalInputs.policy_snapshot_id, - execution_graph_digest: canonicalInputs.execution_graph_digest, - openspec_input_digest: canonicalInputs.openspec_input_digest, + canonical_inputs: canonicalInputs, nodes: canonical.nodes, edges: canonical.edges, }); @@ -227,10 +244,7 @@ function projectAssuranceGraph(input = {}) { nodes: canonical.nodes.map(cloneNode), edges: canonical.edges.map(cloneEdge), }; - const persistedInputs = persistableCanonicalInputs(canonicalInputs); - if (Object.keys(persistedInputs).length > 0) { - resultGraph.canonical_inputs = persistedInputs; - } + resultGraph.canonical_inputs = canonicalInputs; return { ok: true, graph: resultGraph }; } @@ -238,6 +252,8 @@ function projectAssuranceGraph(input = {}) { module.exports = { ALLOWED_RELATIONS, canonicalize, + computeGraphId, + resolveCanonicalInputDigests, rejectForbidden, projectAssuranceGraph, }; diff --git a/scripts/lib/independent-verifier/assessment.js b/scripts/lib/independent-verifier/assessment.js index 8910bf8f..25527b2a 100644 --- a/scripts/lib/independent-verifier/assessment.js +++ b/scripts/lib/independent-verifier/assessment.js @@ -21,7 +21,15 @@ function getAssessmentSchema() { return cachedAssessmentSchema; } +function canonicalizeEvidenceRequirements(value) { + if (!Array.isArray(value) || value.some((token) => typeof token !== "string" || token.length === 0)) { + return null; + } + return [...new Set(value)].sort(); +} + function computeAssessmentId(fields) { + const coverage = canonicalizeEvidenceRequirements(fields.evidence_requirements_satisfied) || []; return sha256Fingerprint("assessment/v1", { schema_version: fields.schema_version, kind: fields.kind, @@ -31,9 +39,33 @@ function computeAssessmentId(fields) { node_id: fields.node_id, candidate_id: fields.candidate_id, policy_snapshot_id: fields.policy_snapshot_id, + evidence_requirements_satisfied: coverage, }); } +function validateAssessment(record) { + if (!record || typeof record !== "object") { + return fail("INVALID_ASSESSMENT", "assessment must be an object"); + } + if (Object.prototype.hasOwnProperty.call(record, "verdict")) { + return fail("MIXED_ASSESSMENT_VERDICT", "assessment must not carry verdict"); + } + const coverage = canonicalizeEvidenceRequirements(record.evidence_requirements_satisfied); + if (!coverage) { + return fail("INVALID_ASSESSMENT", "evidence_requirements_satisfied must be a string array"); + } + const normalized = { ...record, evidence_requirements_satisfied: coverage }; + const validation = validateInstance(getAssessmentSchema(), normalized); + if (!validation.valid) { + return fail("INVALID_ASSESSMENT", validation.errors.map((e) => e.message).join("; ")); + } + const expectedId = computeAssessmentId(normalized); + if (normalized.assessment_id !== expectedId) { + return fail("INVALID_ASSESSMENT", "assessment_id does not match canonical assessment fields"); + } + return { ok: true, assessment: normalized }; +} + /** * Emit an assessment/v1 binding. Verdict is forbidden. * @@ -57,21 +89,23 @@ function emitAssessment(input) { node_id: input.node_id, candidate_id: input.candidate_id, policy_snapshot_id: input.policy_snapshot_id, + evidence_requirements_satisfied: canonicalizeEvidenceRequirements(input.evidence_requirements_satisfied), }; + if (!record.evidence_requirements_satisfied) { + return fail("INVALID_ASSESSMENT", "evidence_requirements_satisfied must be a string array"); + } record.assessment_id = computeAssessmentId(record); if (!SHA256.test(record.assessment_id) || !SHA256.test(record.evidence_id) || !SHA256.test(record.candidate_id)) { return fail("INVALID_ASSESSMENT", "assessment digests must be sha256"); } - const validation = validateInstance(getAssessmentSchema(), record); - if (!validation.valid) { - return fail("INVALID_ASSESSMENT", validation.errors.map((e) => e.message).join("; ")); - } - return { ok: true, assessment: record }; + return validateAssessment(record); } module.exports = { + canonicalizeEvidenceRequirements, computeAssessmentId, emitAssessment, + validateAssessment, }; diff --git a/scripts/lib/independent-verifier/assessment.test.js b/scripts/lib/independent-verifier/assessment.test.js index 601fd962..2ab8ecc7 100644 --- a/scripts/lib/independent-verifier/assessment.test.js +++ b/scripts/lib/independent-verifier/assessment.test.js @@ -13,13 +13,36 @@ const BASE = { node_id: "repair-core", candidate_id: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", policy_snapshot_id: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + evidence_requirements_satisfied: ["ev:test-pass"], }; -test("REQ-independent-verification-006: computeAssessmentId includes role and obligation_id", () => { +test("REQ-independent-verification-006: computeAssessmentId includes role, obligation_id, and canonical coverage", () => { const a = computeAssessmentId({ ...BASE, role: "acceptance" }); const b = computeAssessmentId({ ...BASE, role: "invariants" }); assert.match(a, /^sha256:[a-f0-9]{64}$/); assert.notEqual(a, b); + assert.equal( + computeAssessmentId({ ...BASE, role: "acceptance", evidence_requirements_satisfied: ["b", "a", "a"] }), + computeAssessmentId({ ...BASE, role: "acceptance", evidence_requirements_satisfied: ["a", "b"] }) + ); +}); + +test("REQ-independent-verification-006: evidence_id and obligation_id independently change assessment identity", () => { + const baseline = computeAssessmentId({ ...BASE, role: "acceptance" }); + const differentEvidence = computeAssessmentId({ + ...BASE, + role: "acceptance", + evidence_id: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + }); + const differentObligation = computeAssessmentId({ + ...BASE, + role: "acceptance", + obligation_id: "req-repair-002", + }); + + assert.notEqual(baseline, differentEvidence); + assert.notEqual(baseline, differentObligation); + assert.notEqual(differentEvidence, differentObligation); }); test("REQ-independent-verification-006: emitAssessment validates and rejects verdict", () => { diff --git a/scripts/lib/independent-verifier/bindings.js b/scripts/lib/independent-verifier/bindings.js index b6ac4762..e8d856a4 100644 --- a/scripts/lib/independent-verifier/bindings.js +++ b/scripts/lib/independent-verifier/bindings.js @@ -74,6 +74,14 @@ function validateBindings(input) { return fail("BINDING_MISMATCH", graphBinding.error || graphBinding.reason_code); } + const contract = input.contract; + if (!contract || typeof contract !== "object" || typeof contract.contract_digest !== "string") { + return fail("BINDING_MISMATCH", "input.contract.contract_digest is required"); + } + if (contract.contract_digest !== executionGraph.contract_digest) { + return fail("BINDING_MISMATCH", "input.contract.contract_digest does not match executionGraph.contract_digest"); + } + const repository = input.repository; if (!repository || typeof repository !== "object" || !repository.files) { return fail("BINDING_MISMATCH", "repository bytes are required"); @@ -88,7 +96,7 @@ function validateBindings(input) { return fail("BINDING_MISMATCH", "repository tree does not match candidate_tree"); } - return { ok: true, candidate, executionGraph }; + return { ok: true, candidate, executionGraph, contract }; } module.exports = { diff --git a/scripts/lib/independent-verifier/evidence.js b/scripts/lib/independent-verifier/evidence.js index 9f1aff12..6e444bca 100644 --- a/scripts/lib/independent-verifier/evidence.js +++ b/scripts/lib/independent-verifier/evidence.js @@ -116,12 +116,16 @@ function normalizeEvidence(raw, candidate, executionGraph, harnessCollector) { : raw.obligation_id ? [raw.obligation_id] : []; + const evidenceRequirementsSatisfied = Array.isArray(raw.evidence_requirements_satisfied) + ? [...new Set(raw.evidence_requirements_satisfied.filter((token) => typeof token === "string" && token.length > 0))].sort() + : []; return { ok: true, evidence: record, role: raw.role, obligation_ids: obligationIds, + evidence_requirements_satisfied: evidenceRequirementsSatisfied, }; } diff --git a/scripts/lib/independent-verifier/index.test.js b/scripts/lib/independent-verifier/index.test.js index 2b816e39..847ca46f 100644 --- a/scripts/lib/independent-verifier/index.test.js +++ b/scripts/lib/independent-verifier/index.test.js @@ -103,6 +103,7 @@ function raw(role, bytes, extra = {}) { origin: extra.origin || `role:${role}`, node_id: extra.node_id || "repair-core", obligation_ids: extra.obligation_ids || ["req-repair-001"], + evidence_requirements_satisfied: extra.evidence_requirements_satisfied || ["ev:test-pass"], }; if (Object.prototype.hasOwnProperty.call(extra, "collector") && extra.collector) { record.collector = extra.collector; @@ -181,6 +182,20 @@ test("REQ-independent-verification-001: binding digest mismatch fails closed", ( assert.equal(result.reason_code, "BINDING_MISMATCH"); }); +test("REQ-independent-verification-008: contract digest mismatch fails before strategy or verdict", () => { + const harness = buildHarness(); + const result = verifyCandidate({ + ...harness, + contract: { ...harness.contract, contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" }, + declaredStrategy: "feature", + rawEvidence: [], + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "BINDING_MISMATCH"); + assert.equal(Object.prototype.hasOwnProperty.call(result, "verification"), false); + assert.doesNotMatch(result.error, /strategy|MUST/i); +}); + test("REQ-independent-verification-001: repository tree_digest without bytes is rejected", () => { const harness = buildHarness(); const result = verifyCandidate({ @@ -767,7 +782,7 @@ test("REQ-independent-verification-004: strategy failure short-circuits without assert.notEqual(result.reason_code, "UNFULFILLED_MUST"); }); -test("REQ-independent-verification-006: four roles over one observation yield four assessments and one evidence_id", () => { +test("REQ-independent-verification-006: one observation cannot satisfy four incompatible roles", () => { const harness = buildHarness(); const shared = { origin: "shared-observation", obligation_ids: ["req-repair-001"] }; const bytes = "shared-bytes"; @@ -781,29 +796,27 @@ test("REQ-independent-verification-006: four roles over one observation yield fo raw("negative", bytes, shared), ], }); - assert.equal(result.ok, true, result.error || result.reason_code); - assert.equal(result.assessments.length, 4); - const evidenceIds = new Set(result.evidence.map((ev) => ev.evidence_id)); - assert.equal(evidenceIds.size, 1); - assert.equal(new Set(result.verification.evidence_ids).size, 1); - const assessmentIds = new Set(result.assessments.map((a) => a.assessment_id)); - assert.equal(assessmentIds.size, 4); - const roles = new Set(result.assessments.map((a) => a.role)); - assert.equal(roles.size, 4); - const sample = result.assessments[0]; - assert.equal( - sample.assessment_id, - computeAssessmentId({ - schema_version: 1, - kind: "assessment/v1", - evidence_id: sample.evidence_id, - role: sample.role, - obligation_id: sample.obligation_id, - node_id: sample.node_id, - candidate_id: sample.candidate_id, - policy_snapshot_id: sample.policy_snapshot_id, - }) - ); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "STRATEGY_EVIDENCE_ALIAS"); + assert.equal(Object.prototype.hasOwnProperty.call(result, "verification"), false); +}); + +test("REQ-independent-verification-006: strict-tdd and bug role order fail closed", () => { + const harness = buildHarness(); + const strict = verifyCandidate({ + ...harness, + rawEvidence: [raw("green", "green"), raw("red", "red")], + }); + assert.equal(strict.ok, false); + assert.equal(strict.reason_code, "STRATEGY_SEQUENCE_VIOLATION"); + + const bug = verifyCandidate({ + ...harness, + declaredStrategy: "bug", + rawEvidence: [raw("patch", "patch"), raw("red", "red"), raw("green", "green")], + }); + assert.equal(bug.ok, false); + assert.equal(bug.reason_code, "STRATEGY_SEQUENCE_VIOLATION"); }); test("FABRICATED_EVIDENCE: non-object raw and missing origin fail closed", () => { diff --git a/scripts/lib/independent-verifier/obligation-coverage.js b/scripts/lib/independent-verifier/obligation-coverage.js index d57a275c..ac0cd18f 100644 --- a/scripts/lib/independent-verifier/obligation-coverage.js +++ b/scripts/lib/independent-verifier/obligation-coverage.js @@ -23,6 +23,15 @@ function isMust(obligation) { return String((obligation && obligation.criticality) || "must").toLowerCase() === "must"; } +function normalizedCoverage(item, requiredEvidence) { + const supplied = item && (item.evidence_requirements_satisfied || (item.evidence && item.evidence.evidence_requirements_satisfied)); + const satisfied = Array.isArray(supplied) + ? supplied.filter((token) => typeof token === "string") + : []; + const required = new Set(requiredEvidence); + return [...new Set(satisfied.filter((token) => required.has(token)))].sort(); +} + /** * Walk non-deferred MUST obligations after strategy evaluation. * Join key is persistable obligation_id, not K4a evidence tokens. @@ -96,6 +105,15 @@ function walkMustObligations(input) { return fail("UNFULFILLED_MUST", `MUST obligation ${obligation.id} has no admissible evidence`); } + const satisfiedTokens = new Set(); + for (const item of admissible) { + for (const token of normalizedCoverage(item, requiredEvidence)) satisfiedTokens.add(token); + } + const missingTokens = requiredEvidence.filter((token) => !satisfiedTokens.has(token)); + if (missingTokens.length > 0) { + return fail("UNFULFILLED_MUST", `MUST obligation ${obligation.id} is missing required evidence: ${missingTokens.join(", ")}`); + } + for (const item of admissible) { const emitted = emitAssessment({ evidence_id: item.evidence.evidence_id, @@ -104,6 +122,7 @@ function walkMustObligations(input) { node_id: item.evidence.node_id, candidate_id: candidate && candidate.candidate_id, policy_snapshot_id: policySnapshotId, + evidence_requirements_satisfied: normalizedCoverage(item, requiredEvidence), }); if (!emitted.ok) return emitted; assessments.push(emitted.assessment); @@ -115,5 +134,6 @@ function walkMustObligations(input) { module.exports = { isApprovedDeferred, + normalizedCoverage, walkMustObligations, }; diff --git a/scripts/lib/independent-verifier/obligation-coverage.test.js b/scripts/lib/independent-verifier/obligation-coverage.test.js index df1be529..05a785e8 100644 --- a/scripts/lib/independent-verifier/obligation-coverage.test.js +++ b/scripts/lib/independent-verifier/obligation-coverage.test.js @@ -11,6 +11,7 @@ const EVIDENCE = { evidence_id: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", provenance: "runtime-observed", node_id: "repair-core", + evidence_requirements_satisfied: ["ev:test-pass"], }; function graph(obligations) { @@ -141,6 +142,31 @@ test("REQ-independent-verification-005: strategy-shaped bindings still emit pers assert.equal(result.assessments[0].role, "acceptance"); }); +test("REQ-independent-verification-005: token subset coverage rejects partial bindings and persists the complete union", () => { + const base = { + id: "req-repair-001", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: ["ev:a", "ev:b"], + }; + const partial = walkMustObligations({ + classified: [{ role: "acceptance", evidence: EVIDENCE, obligation_ids: [base.id], evidence_requirements_satisfied: ["ev:a"] }], + executionGraph: graph([base]), candidate: CANDIDATE, policySnapshotId: POLICY, + }); + assert.equal(partial.ok, false); + assert.equal(partial.reason_code, "UNFULFILLED_MUST"); + + const complete = walkMustObligations({ + classified: [ + { role: "acceptance", evidence: EVIDENCE, obligation_ids: [base.id], evidence_requirements_satisfied: ["ev:b"] }, + { role: "invariants", evidence: { ...EVIDENCE, evidence_id: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, obligation_ids: [base.id], evidence_requirements_satisfied: ["ev:a"] }, + ], + executionGraph: graph([base]), candidate: CANDIDATE, policySnapshotId: POLICY, + }); + assert.equal(complete.ok, true, complete.error); + assert.deepEqual(complete.assessments.map((assessment) => assessment.evidence_requirements_satisfied), [["ev:b"], ["ev:a"]]); +}); + test("REQ-independent-verification-005: weak provenance on MUST is INSUFFICIENT_PROVENANCE", () => { const runtimeUnbound = { ...EVIDENCE, diff --git a/scripts/lib/independent-verifier/strategy-policy.js b/scripts/lib/independent-verifier/strategy-policy.js index b060db68..f83de837 100644 --- a/scripts/lib/independent-verifier/strategy-policy.js +++ b/scripts/lib/independent-verifier/strategy-policy.js @@ -114,6 +114,39 @@ function failIfInadmissible(policy, role, items, message) { return null; } +function assertDistinctRoleEvidence(items) { + const rolesByEvidenceId = new Map(); + for (const item of items || []) { + const evidenceId = item && item.evidence && item.evidence.evidence_id; + if (typeof evidenceId !== "string" || typeof item.role !== "string") continue; + const roles = rolesByEvidenceId.get(evidenceId) || new Set(); + roles.add(item.role); + rolesByEvidenceId.set(evidenceId, roles); + if (roles.size > 1) { + return fail("STRATEGY_EVIDENCE_ALIAS", `evidence_id ${evidenceId} cannot satisfy distinct strategy roles`); + } + } + return { ok: true }; +} + +function assertRoleOrder(strategyName, items) { + const positions = new Map(); + for (let index = 0; index < (items || []).length; index += 1) { + const role = items[index] && items[index].role; + if (!positions.has(role)) positions.set(role, []); + positions.get(role).push(index); + } + const ordered = strategyName === "bug" ? ["red", "patch", "green"] : strategyName === "strict-tdd" ? ["red", "green"] : []; + for (let index = 0; index < ordered.length - 1; index += 1) { + const earlier = positions.get(ordered[index]) || []; + const later = positions.get(ordered[index + 1]) || []; + if (earlier.length > 0 && later.length > 0 && Math.max(...earlier) > Math.min(...later)) { + return fail("STRATEGY_SEQUENCE_VIOLATION", `${ordered[index]} evidence must precede ${ordered[index + 1]} evidence`); + } + } + return { ok: true }; +} + /** * Check anyOf role groups and their provenance admission. * Extracted so evaluateStrategy stays at most three control-flow levels. @@ -148,6 +181,11 @@ function evaluateStrategy(strategyName, items) { if (!policy) return fail("MISSING_STRATEGY_MINIMUM", `unknown strategy ${strategyName}`); const roles = rolesOf(items); + const distinctEvidence = assertDistinctRoleEvidence(items); + if (!distinctEvidence.ok) return distinctEvidence; + const roleOrder = assertRoleOrder(strategyName, items); + if (!roleOrder.ok) return roleOrder; + if (strategyName === "feature" && policy.characterizationOnlyRoles) { const hasChar = policy.characterizationOnlyRoles.some((role) => roles.has(role)); const hasFeatureMin = roles.has("acceptance"); @@ -210,4 +248,6 @@ module.exports = { STRATEGY_TABLE, selectStrategy, evaluateStrategy, + assertDistinctRoleEvidence, + assertRoleOrder, }; diff --git a/scripts/lib/k2a-maturity-docs.test.js b/scripts/lib/k2a-maturity-docs.test.js index 1586bdb2..bed765dd 100644 --- a/scripts/lib/k2a-maturity-docs.test.js +++ b/scripts/lib/k2a-maturity-docs.test.js @@ -42,6 +42,8 @@ test("K2a maturity docs tag host surfaces implemented; Candidate/attestation/del assert.doesNotMatch(arch, /\{implemented\}.*CandidateEvaluationAttestation/); assert.doesNotMatch(roadmap, /\|\s*`in-progress`\s*\|\s*\*\*K4b\*\*/); assert.doesNotMatch(roadmap, /\|\s*`in-progress`\s*\|\s*\*\*K6b\*\*/); - assert.doesNotMatch(roadmap, /\|\s*`blocked`\s*\|\s*K6b\b/); + assert.doesNotMatch(roadmap, /\|\s*`blocked`\s*\|\s*\*\*K6b\*\*/); assert.doesNotMatch(roadmap, /\|\s*`next-eligible`\s*\|\s*\*\*K6b\*\*/); + assert.doesNotMatch(roadmap, /\|\s*`revise`\s*\|\s*\*\*K6b\*\*/); + assert.doesNotMatch(roadmap, /\|\s*`blocked-by-K6b-remediation`\s*\|\s*\*\*K6c\*\*/); }); diff --git a/scripts/lib/k6b-schema-fixtures.test.js b/scripts/lib/k6b-schema-fixtures.test.js index 19ef9e30..2330d18b 100644 --- a/scripts/lib/k6b-schema-fixtures.test.js +++ b/scripts/lib/k6b-schema-fixtures.test.js @@ -105,6 +105,7 @@ test("K6b contract claims: additive families list required fields without replac "node_id", "candidate_id", "policy_snapshot_id", + "evidence_requirements_satisfied", ]); }); @@ -248,6 +249,8 @@ test("K6b assessment/v1: valid fixture passes; verdict, missing fields, and cros const missing = readJson("schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json"); assert.equal(validateInstance(assessmentSchema, missing).valid, false); + const missingCoverage = readJson("schemas/kernel/assessment/fixtures/invalid/v1-missing-coverage.json"); + assert.equal(validateInstance(assessmentSchema, missingCoverage).valid, false); const withVerdict = readJson("schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json"); const verdictRes = validateInstance(assessmentSchema, withVerdict);