From f3d4073d5dadf42db7bc8ab74af72961c3fb3ca0 Mon Sep 17 00:00:00 2001 From: Manuel Retamozo Date: Thu, 27 Aug 2026 21:44:15 +0200 Subject: [PATCH] chore(release): actualizar version a 2.51.0 y changelog Cierra la remediacion de integridad K6b (assessment/v1, MUST walk, collector de harness, graph_id) y alinea el roadmap: K6b done, K6c next-eligible. --- .claude-plugin/plugin.json | 2 +- .plugin.json | 2 +- CHANGELOG.md | 11 + ...60827-007-additive-assessment-family-id.md | 23 + ...walk-after-strategy-assessment-identity.md | 23 + ...ollector-transport-provenance-allowlist.md | 23 + ...nonical-graph-id-fail-closed-projection.md | 23 + docs/architecture/harness-evolution.md | 6 +- docs/roadmaps/harness-evolution.md | 14 +- .../.4r/create-successor.js | 121 + .../.4r/derived.json | 172 + .../.4r/evidence.json | 147 + .../.4r/fail-and-retry-slice1.js | 51 + .../.4r/freeze-lineage.js | 115 + .../.4r/generalist.json | 5 + .../.4r/lens-readability-findings.json | 27 + .../.4r/lens-reliability-findings.json | 12 + .../.4r/lens-resilience-findings.json | 1 + .../.4r/lens-risk-findings.json | 1 + .../.4r/lineage.json | 334 ++ .../.4r/manifest-v2.json | 34 + .../.4r/migrate-and-begin-slice1.js | 69 + .../.4r/normalize-evidence.js | 145 + .../.4r/pass-slice1-begin-slice2.js | 66 + .../.4r/pass-slice2.js | 46 + .../.4r/persist-gate.js | 48 + .../.4r/planned-gate.json | 178 + .../.4r/planned-lineage.json | 14 + .../.4r/predecessor-lineage.json | 661 +++ .../.4r/record-lenses.js | 42 + .../.4r/record-slice1-retry2.js | 48 + .../.4r/record-slice1.js | 51 + .../.4r/record-slice2.js | 48 + .../.4r/unified.diff | 3599 +++++++++++++++++ .../apply-progress.md | 100 + .../archive-plan.json | 102 + .../archive-report.md | 75 + .../decisions/adr-001.md | 23 + .../decisions/adr-002.md | 23 + .../decisions/adr-003.md | 23 + .../decisions/adr-004.md | 23 + .../design.md | 210 + .../prepared-specs/assurance-graph/spec.md | 168 + .../independent-verification/spec.md | 228 ++ .../kernel-contract-schemas/spec.md | 847 ++++ .../proposal.md | 74 + .../specs/assurance-graph/spec.md | 119 + .../specs/independent-verification/spec.md | 158 + .../specs/kernel-contract-schemas/spec.md | 120 + .../state.yaml | 170 + .../tasks.md | 122 + .../verify-report.md | 199 + openspec/config.yaml | 2 +- openspec/specs/assurance-graph/spec.md | 94 +- .../specs/independent-verification/spec.md | 117 +- .../specs/kernel-contract-schemas/spec.md | 108 +- package.json | 2 +- .../fixtures/invalid/v1-evidence-alias.json | 10 + .../fixtures/invalid/v1-missing-required.json | 5 + .../fixtures/invalid/v1-with-verdict.json | 12 + .../fixtures/valid/v1-complete.json | 11 + .../fixtures/valid/v1-four-roles.json | 46 + schemas/kernel/assessment/v1.schema.json | 58 + schemas/kernel/assurance-graph/v1.schema.json | 25 + schemas/kernel/contract-claims.json | 15 + schemas/kernel/manifest.json | 5 + .../k6b-verifier-assurance-graph-e2e.test.js | 40 +- scripts/lib/assurance-graph/index.js | 24 +- scripts/lib/assurance-graph/index.test.js | 149 +- scripts/lib/assurance-graph/projector.js | 110 +- .../lib/contract-checkers/k1-schema-compat.js | 4 + .../lib/independent-verifier/assessment.js | 77 + .../independent-verifier/assessment.test.js | 45 + .../collector-provenance.js | 86 + scripts/lib/independent-verifier/evidence.js | 14 +- scripts/lib/independent-verifier/index.js | 115 +- .../lib/independent-verifier/index.test.js | 332 +- .../obligation-coverage.js | 119 + .../obligation-coverage.test.js | 272 ++ scripts/lib/k1-scope-guard.test.js | 1 + scripts/lib/k6b-schema-fixtures.test.js | 77 + scripts/lib/lifecycle-kernel/k1-compat.js | 1 + 82 files changed, 10762 insertions(+), 160 deletions(-) create mode 100644 docs/adr/adr-20260827-007-additive-assessment-family-id.md create mode 100644 docs/adr/adr-20260827-008-must-walk-after-strategy-assessment-identity.md create mode 100644 docs/adr/adr-20260827-009-collector-transport-provenance-allowlist.md create mode 100644 docs/adr/adr-20260827-010-canonical-graph-id-fail-closed-projection.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/create-successor.js create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/derived.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/evidence.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/fail-and-retry-slice1.js create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/freeze-lineage.js create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/generalist.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-readability-findings.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-reliability-findings.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-resilience-findings.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-risk-findings.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lineage.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/manifest-v2.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/migrate-and-begin-slice1.js create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/normalize-evidence.js create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/pass-slice1-begin-slice2.js create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/pass-slice2.js create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/persist-gate.js create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/planned-gate.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/planned-lineage.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/predecessor-lineage.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-lenses.js create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-slice1-retry2.js create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-slice1.js create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-slice2.js create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/unified.diff create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/apply-progress.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/archive-plan.json create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/archive-report.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-001.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-002.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-003.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-004.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/design.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/prepared-specs/assurance-graph/spec.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/prepared-specs/independent-verification/spec.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/prepared-specs/kernel-contract-schemas/spec.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/proposal.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/specs/assurance-graph/spec.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/specs/independent-verification/spec.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/state.yaml create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/tasks.md create mode 100644 openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/verify-report.md create mode 100644 schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json create mode 100644 schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json create mode 100644 schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json create mode 100644 schemas/kernel/assessment/fixtures/valid/v1-complete.json create mode 100644 schemas/kernel/assessment/fixtures/valid/v1-four-roles.json create mode 100644 schemas/kernel/assessment/v1.schema.json create mode 100644 scripts/lib/independent-verifier/assessment.js create mode 100644 scripts/lib/independent-verifier/assessment.test.js create mode 100644 scripts/lib/independent-verifier/collector-provenance.js create mode 100644 scripts/lib/independent-verifier/obligation-coverage.js create mode 100644 scripts/lib/independent-verifier/obligation-coverage.test.js diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index c379760e..87a5b151 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.50.0", + "version": "2.51.0", "author": { "name": "Manuel Michael Retamozo García" }, diff --git a/.plugin.json b/.plugin.json index c379760e..87a5b151 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.50.0", + "version": "2.51.0", "author": { "name": "Manuel Michael Retamozo García" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 9df063e3..e078e438 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.51.0] - 2026-08-27 + +### Added +- **Remediación de integridad K6b (`k6b-verification-integrity-remediation`)**: + - PASS exige evidencia admisible por cada obligación MUST del Execution Graph; `strategy satisfied ≠ Execution Graph satisfied`. + - Contrato aditivo `assessment/v1` (`ospec://schemas/kernel/assessment/v1`): binding persistible EvidenceId ↔ role ↔ obligation_id ↔ node_id ↔ policy, sin mutar `evidence/v2` ni K1 v1. + - Provenance fuerte derivada del canal del harness (`input.collector` / `input.collectors[]`); `collector` en el sobre del worker falla cerrado (`UNTRUSTED_COLLECTOR`). Desacuerdo claim↔canal simétrico; weak+allowlist no escala a clase fuerte. + - `graph_id` fingerprinta inputs canónicos; proyección fail-closed (`GRAPH_PROJECTION_FAILED` / `GRAPH_DIVERGENCE`); `rejectForbidden` por kind/namespace. + - ADRs `docs/adr/adr-20260827-007` a `010`. Specs `independent-verification`, `assurance-graph` y `kernel-contract-schemas`. + - K6b queda `done`; K6c pasa a `next-eligible`. Ciclo SDD completo (ruta standard, high-risk, size:exception, 4R successor approved). Verify: 2754 pass, 0 fail. Archivado en `openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/`. + ## [2.50.0] - 2026-08-27 ### Added diff --git a/docs/adr/adr-20260827-007-additive-assessment-family-id.md b/docs/adr/adr-20260827-007-additive-assessment-family-id.md new file mode 100644 index 00000000..ec76618e --- /dev/null +++ b/docs/adr/adr-20260827-007-additive-assessment-family-id.md @@ -0,0 +1,23 @@ +# ADR-001: Additive assessment family `$id` + +- Status: proposed +- Change: k6b-verification-integrity-remediation +- Date: 2026-08-27 + +## Context + +Assumption `sdd-propose-001` left the persistable binding `$id` to design. Intent forbids in-place mutation of `evidence/v2` and K1 v1. Assessment identity must include `role` and `obligation_id` without collapsing physical `EvidenceId`. + +## Decision + +Publish family `assessment` at `$id` `ospec://schemas/kernel/assessment/v1`, `kind: "assessment/v1"`, path `schemas/kernel/assessment/v1.schema.json`, `schema_version: 1`. Register additively in `manifest.json` and `contract-claims.json`. Keep `evidence/v2`, `verification/v2`, and K1 v1 bytes/pins frozen. + +## Alternatives + +- Mutate `evidence/v2` to carry role/obligation: rejected; observation identity would absorb evaluation identity. +- Evolve `verification/v3` with embedded bindings: rejected; verdict unique-sort of `evidence_ids` still hides distinct roles. +- `$id` `ospec://schemas/kernel/assessment-binding/v1`: rejected; breaks the `{noun}/vN` kernel `$id` pattern. + +## Consequences + +Consumers pin a third K6b-era family without migrating evidence. Rollback deletes the additive family. Four roles over one observation become four `assessment_id` values by construction. diff --git a/docs/adr/adr-20260827-008-must-walk-after-strategy-assessment-identity.md b/docs/adr/adr-20260827-008-must-walk-after-strategy-assessment-identity.md new file mode 100644 index 00000000..4137374f --- /dev/null +++ b/docs/adr/adr-20260827-008-must-walk-after-strategy-assessment-identity.md @@ -0,0 +1,23 @@ +# ADR-002: MUST walk after strategy; assessment identity ≠ EvidenceId + +- Status: proposed +- Change: k6b-verification-integrity-remediation +- Date: 2026-08-27 + +## Context + +`evaluateStrategy()` only checks role minimums. PASS was therefore possible without Obligation Manifest coverage. `obligation_ids` on classified evidence are ephemeral, so `satisfies` edges cannot be replayed from `evidence/v2`. + +## Decision + +Keep `evaluateStrategy` as the role-shape gate. After it succeeds, walk every non-deferred `must` obligation, emit `assessment/v1` records, and require those assessments for PASS. `computeEvidenceId` stays observation-only. `assessment_id` fingerprints `evidence_id`, `role`, `obligation_id`, `node_id`, `candidate_id`, and `policy_snapshot_id`. + +## Alternatives + +- Fold MUST coverage into `evaluateStrategy`: rejected; mixes strategy policy with graph obligations and reason codes. +- Walk obligations before strategy: rejected; delays cheap role failures. +- Treat unique-sort of `verification.evidence_ids` as binding identity: rejected; one EvidenceId used as four roles would collapse. + +## Consequences + +Reason codes split cleanly (`MISSING_STRATEGY_MINIMUM` vs `UNFULFILLED_MUST` / `UNKNOWN_OBLIGATION_ID` / `WRONG_IMPLEMENTING_NODE`). Projector `satisfies` edges consume assessments only. Existing tests that omit obligation bindings will fail closed until updated. diff --git a/docs/adr/adr-20260827-009-collector-transport-provenance-allowlist.md b/docs/adr/adr-20260827-009-collector-transport-provenance-allowlist.md new file mode 100644 index 00000000..12cc7e75 --- /dev/null +++ b/docs/adr/adr-20260827-009-collector-transport-provenance-allowlist.md @@ -0,0 +1,23 @@ +# ADR-003: Strong provenance from collector/transport allowlist + +- Status: proposed +- Change: k6b-verification-integrity-remediation +- Date: 2026-08-27 + +## Context + +`normalizeEvidence` currently copies `raw.provenance` into `evidence/v2`. A worker can claim `runtime-observed`. PKI is out of scope. Payload digest identifies bytes, not origin. + +## Decision + +Derive strong classes (`runtime-observed`, `host-attested`, `tool-produced`) from harness-supplied `collector.id` + `transport` via a fail-closed allowlist (`node-test`/`npm-test`/`node:test` → runtime-observed; `tool-execution` → tool-produced; `host-adapter` → host-attested). Worker/absent/unknown collectors cannot produce a strong class. Store the derived class on `evidence/v2`; never copy collector metadata onto that record. + +## Alternatives + +- Trust the payload provenance string: rejected; this is the verified defect. +- Require signatures or PKI: rejected; out of scope for this change. +- Treat payload digest as origin: rejected; digest is content identity only. + +## Consequences + +Callers that claim a strong class must pass allowlisted collector metadata (tests included). Mis-mapped collector ids fail closed rather than silently weakening provenance. diff --git a/docs/adr/adr-20260827-010-canonical-graph-id-fail-closed-projection.md b/docs/adr/adr-20260827-010-canonical-graph-id-fail-closed-projection.md new file mode 100644 index 00000000..595329bc --- /dev/null +++ b/docs/adr/adr-20260827-010-canonical-graph-id-fail-closed-projection.md @@ -0,0 +1,23 @@ +# ADR-004: Canonical graph_id fingerprint and fail-closed projection + +- Status: proposed +- Change: k6b-verification-integrity-remediation +- Date: 2026-08-27 + +## Context + +`graph_id` currently hashes only `candidate_id` plus canonical nodes/edges. Contract or policy changes do not diverge. `verifyCandidate` returns `ok: true` when projection fails, omitting the graph. `rejectForbidden` substring-matches `id`, so `REQ-add-authorization-header` is rejected. + +## Decision + +Fingerprint `contract_digest`, `policy_snapshot_id`, Execution Graph digest, canonical OpenSpec input, `candidate_id`, and canonical nodes/edges in `graph_id`. Persist those digests as optional `canonical_inputs` on `assurance-graph/v1`. `verifyCandidate` requires a successful projection before `ok: true`. Match forbidden subjects by `kind`/`namespace`, never by `id` substring. + +## Alternatives + +- Keep the current `graph_id` preimage: rejected; replay cannot detect contract/policy drift. +- Leave projection optional on the facade: rejected; PASS without a graph is the integrity hole. +- Continue substring markers on `id`: rejected; legitimate requirement ids containing `authorization` fail. + +## Consequences + +Existing runtime `graph_id` values change. Replay becomes possible from persistable outputs. Facade tests that ignored projector failure must assert `GRAPH_PROJECTION_FAILED` / `GRAPH_DIVERGENCE`. diff --git a/docs/architecture/harness-evolution.md b/docs/architecture/harness-evolution.md index a81e2282..71cb5aad 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.50.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** están entregados y archivados. OpenSpec/Git/Candidate siguen siendo la única autoridad semántica; el Assurance Graph es proyección. **K6c** es la siguiente iniciativa elegible. +> **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`. > **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)~~ — verifier, strategies/provenance y proyección implementados; autoridad del grafo, K6c ChallengePlan y K6d complexity delta siguen pendientes. +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. 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 8233c05d..57f5adf5 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.50.0, 2026-08-27. +> **Versión de referencia:** v2.51.0, 2026-08-27. > **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,7 +78,7 @@ 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 como proyección derivada (no autoridad); archivado y publicado en v2.50.0 (`2026-08-27-k6b-verifier-evidence-assurance-graph`) | +| `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 | | `pending` | K6d–K8 | Complexity delta, review authority, **Evaluation Attestation** | | `pending` | K9 | Gate de promoción shadow/replay/A-B (checkpoints intermedios ya validados) | @@ -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-eligible: K6c +Next: K6c (next-eligible) ↓ Pending: K6d → K7 → K8 ↓ @@ -1011,7 +1011,7 @@ Vertical Repair shadow produce Candidate congelado. Gate de invariantes cerrado **Dependencias:** K4b + K6a + K3. -**Estado:** `done`. Change OpenSpec `k6b-verifier-evidence-assurance-graph` archivado y publicado en v2.50.0: verifier independiente, strategies/provenance y Assurance Graph como proyección derivada. 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`); 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`.** **Absorbe/rebasa:** P12/P16; O15; separación apply/verify vigente. @@ -1103,9 +1103,9 @@ 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`. K6c no empieza antes. +**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) — **pending** +### K6c — adversarial challenges (policy-selected) — **next-eligible** **Dependencias:** K6b (+ `PolicySnapshot` / strategy de evidencia). @@ -2060,3 +2060,5 @@ Un Change Program (objetivo → children OpenSpec + cursor, ver investigación ` - 2026-08-27: reconciliación aditiva (sin mover ruta crítica ni next-eligible): se nombra la distinción proceso intra-change vs Change Program inter-change; first-match de la tabla viva es compatibilidad, no K10; K6b/R4/K10 no absorben ese hueco. Investigación no normativa `docs/architecture/research/proportional-process-and-change-program.md`. Arquitectura: corte conceptual de la misma fecha; deuda stale K3/K4a/K5/K6a/K4b reconciliada. - 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. diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/create-successor.js b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/create-successor.js new file mode 100644 index 00000000..3a2ffc84 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/create-successor.js @@ -0,0 +1,121 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const { execFileSync } = require("child_process"); +const { + deriveReviewDimensions, + validateReviewDecision, + validateGeneralistDecision, +} = require("../../../../scripts/lib/review-dimensions.js"); +const { planReviewGate, planLineageGate } = require("../../../../scripts/lib/review-gate-state.js"); +const { createSuccessor, beginLens } = require("../../../../scripts/lib/review-lineage.js"); + +const DIR = __dirname; +const ROOT = path.resolve(__dirname, "../../../.."); +const packed = JSON.parse(fs.readFileSync(path.join(DIR, "evidence.json"), "utf8")); +const evidence = packed.evidence; +const predecessor = JSON.parse(fs.readFileSync(path.join(DIR, "predecessor-lineage.json"), "utf8")); +const generalist = JSON.parse(fs.readFileSync(path.join(DIR, "generalist.json"), "utf8")); + +const gv = validateGeneralistDecision(generalist); +if (!gv.valid) { + console.error(gv); + process.exit(1); +} + +const decision = deriveReviewDimensions(evidence, generalist); +const dv = validateReviewDecision(decision); +if (!dv.valid) { + console.error(dv); + process.exit(1); +} + +fs.writeFileSync(path.join(DIR, "derived.json"), JSON.stringify(decision, null, 2)); + +const planned = planReviewGate({ + routeGates: ["clarify", "4r-review-gate"], + existingGate: {}, + decision, + validationErrors: [], +}); +fs.writeFileSync(path.join(DIR, "planned-gate.json"), JSON.stringify(planned, null, 2)); + +function sha256Bytes(buf) { + return `sha256:${crypto.createHash("sha256").update(buf).digest("hex")}`; +} + +const diff = fs.readFileSync(path.join(DIR, "unified.diff")); +const paths = packed.paths.slice().sort(); +const fileDigests = {}; +for (const rel of paths) { + fileDigests[rel] = sha256Bytes(fs.readFileSync(path.join(ROOT, rel))); +} +const candidateTree = sha256Bytes(Buffer.from(JSON.stringify(fileDigests))); +const pathsDigest = sha256Bytes(Buffer.from(paths.join("\n"))); +const baseTree = execFileSync("git", ["rev-parse", "HEAD^{tree}"], { cwd: ROOT, encoding: "utf8" }).trim(); + +const candidate = { + projection: "workspace", + base_tree: baseTree, + candidate_tree: candidateTree, + paths, + diff_hash: sha256Bytes(diff), + paths_digest: pathsDigest, + authored_lines: packed.changed_lines.added, + original_changed_lines: packed.changed_lines.total, +}; + +const approvals = [ + { + id: "k6b-verification-integrity-remediation-bounded-review-001", + applies_to: ["sdd-design", "sdd-tasks", "sdd-apply", "sdd-verify"], + }, +]; + +let lineage = createSuccessor(predecessor, { + classification: "high-risk", + evidence_fingerprint: evidence.fingerprint, + selected_dimensions: decision.selected_specialists, + candidate, + authority_kind: "new-candidate", + reason: "new-candidate after harness collector migration and remaining advisory closures", + approval_reference: "k6b-verification-integrity-remediation-bounded-review-001", + approvals, +}); + +const dimensions = decision.selected_specialists; +for (const dimension of dimensions) { + lineage = beginLens(lineage, { + dimension, + request_id: `lens-${dimension}-001`, + expected_revision: lineage.revision, + }); +} + +fs.writeFileSync(path.join(DIR, "lineage.json"), JSON.stringify(lineage, null, 2)); +const lineagePlan = planLineageGate({ + lineage, + observed_candidate_id: lineage.current_candidate_id, + downstream_gate: "status", +}); +fs.writeFileSync(path.join(DIR, "planned-lineage.json"), JSON.stringify(lineagePlan, null, 2)); + +console.log( + JSON.stringify( + { + predecessor_id: predecessor.lineage_id, + predecessor_status: predecessor.status, + generation: lineage.generation, + lineage_id: lineage.lineage_id, + candidate_id: lineage.current_candidate_id, + revision: lineage.revision, + selected: decision.selected_specialists, + budget: lineage.correction_budget, + next_action: lineagePlan.next_action || lineagePlan, + }, + null, + 2 + ) +); diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/derived.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/derived.json new file mode 100644 index 00000000..9c21b15c --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/derived.json @@ -0,0 +1,172 @@ +{ + "schema_version": 1, + "classification": "high-risk", + "evidence": { + "schema_version": 1, + "fingerprint": "sha256:66b1b1c10af6adfd036aa014032450b530160d81f05f9dae3af5c1cf09ade779", + "sources": { + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md", + "openspec/changes/k6b-verification-integrity-remediation/design.md", + "openspec/changes/k6b-verification-integrity-remediation/proposal.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/state.yaml", + "openspec/changes/k6b-verification-integrity-remediation/tasks.md", + "openspec/changes/k6b-verification-integrity-remediation/verify-report.md", + "schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-required.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/assurance-graph/v1.schema.json", + "schemas/kernel/contract-claims.json", + "schemas/kernel/manifest.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/contract-checkers/k1-schema-compat.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.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/k1-scope-guard.test.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js", + "scripts/lib/lifecycle-kernel/k1-compat.js" + ], + "capabilities": [ + "runtime" + ], + "operation_types": [ + "add", + "modify" + ], + "dependencies": [], + "facts": [ + { + "code": "design-risk", + "source": "design", + "detail": "verification-integrity-trust-boundary" + }, + { + "code": "metadata-runtime", + "source": "metadata", + "detail": "runtime" + }, + { + "code": "diff-auth-permission", + "source": "real-diff", + "detail": "scripts/lib/independent-verifier/assessment.js,scripts/lib/independent-verifier/obligation-coverage.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/assessment.js,scripts/lib/independent-verifier/obligation-coverage.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": "verification-integrity-trust-boundary", + "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" + ] +} \ No newline at end of file diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/evidence.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/evidence.json new file mode 100644 index 00000000..a32d2bb2 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/evidence.json @@ -0,0 +1,147 @@ +{ + "evidence": { + "schema_version": 1, + "classification": "high-risk", + "fingerprint": "sha256:66b1b1c10af6adfd036aa014032450b530160d81f05f9dae3af5c1cf09ade779", + "sources": { + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md", + "openspec/changes/k6b-verification-integrity-remediation/design.md", + "openspec/changes/k6b-verification-integrity-remediation/proposal.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/state.yaml", + "openspec/changes/k6b-verification-integrity-remediation/tasks.md", + "openspec/changes/k6b-verification-integrity-remediation/verify-report.md", + "schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-required.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/assurance-graph/v1.schema.json", + "schemas/kernel/contract-claims.json", + "schemas/kernel/manifest.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/contract-checkers/k1-schema-compat.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.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/k1-scope-guard.test.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js", + "scripts/lib/lifecycle-kernel/k1-compat.js" + ], + "capabilities": [ + "runtime" + ], + "operation_types": [ + "add", + "modify" + ], + "dependencies": [], + "facts": [ + { + "code": "design-risk", + "source": "design", + "detail": "verification-integrity-trust-boundary" + }, + { + "code": "metadata-runtime", + "source": "metadata", + "detail": "runtime" + }, + { + "code": "diff-auth-permission", + "source": "real-diff", + "detail": "scripts/lib/independent-verifier/assessment.js,scripts/lib/independent-verifier/obligation-coverage.js" + } + ] + } + }, + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md", + "openspec/changes/k6b-verification-integrity-remediation/design.md", + "openspec/changes/k6b-verification-integrity-remediation/proposal.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/state.yaml", + "openspec/changes/k6b-verification-integrity-remediation/tasks.md", + "openspec/changes/k6b-verification-integrity-remediation/verify-report.md", + "schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-required.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/assurance-graph/v1.schema.json", + "schemas/kernel/contract-claims.json", + "schemas/kernel/manifest.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/contract-checkers/k1-schema-compat.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.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/k1-scope-guard.test.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js", + "scripts/lib/lifecycle-kernel/k1-compat.js" + ], + "operationTypes": [ + "modify", + "add" + ], + "changed_lines": { + "added": 2892, + "removed": 111, + "total": 3003 + }, + "file_count": 41, + "facts": [ + { + "code": "design-risk", + "source": "design", + "detail": "verification-integrity-trust-boundary" + }, + { + "code": "metadata-runtime", + "source": "metadata", + "detail": "runtime" + }, + { + "code": "diff-auth-permission", + "source": "real-diff", + "detail": "scripts/lib/independent-verifier/assessment.js,scripts/lib/independent-verifier/obligation-coverage.js" + } + ] +} \ No newline at end of file diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/fail-and-retry-slice1.js b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/fail-and-retry-slice1.js new file mode 100644 index 00000000..242cea3c --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/fail-and-retry-slice1.js @@ -0,0 +1,51 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const { applyTargetedValidation, beginCorrection, nextLineageAction } = require("../../../../scripts/lib/review-lineage.js"); + +const lineagePath = path.join(__dirname, "lineage.json"); +let lineage = JSON.parse(fs.readFileSync(lineagePath, "utf8")); + +lineage = applyTargetedValidation(lineage, { + request_id: "slice-1-validation-001", + expected_revision: lineage.revision, + slice_id: lineage.active_slice_id, + outcomes: [{ id: "F-d5739d79237afeb8", status: "unresolved" }], + regression: { + detected: false, + evidence: [ + "static: resolveEvidenceProvenance still reads raw.collector from the same object as bytes/origin", + "apply-progress: 48 focal tests pass; no passed slices exist", + ], + impacted_slices: [], + }, + follow_ups: [], +}); + +const action = nextLineageAction(lineage); +const remaining = 200 - lineage.correction_slices["S-ea4088e8a61de9f8"].used_lines; + +if (action.type === "correct" && action.slice_id === "S-ea4088e8a61de9f8") { + const slice = lineage.correction_slices[action.slice_id]; + lineage = beginCorrection(lineage, { + request_id: "slice-1-correction-start-002", + expected_revision: lineage.revision, + slice_id: action.slice_id, + finding_ids: slice.finding_ids, + paths: slice.permitted_paths, + base_candidate_id: lineage.current_candidate_id, + forecast_lines: remaining, + }); +} + +fs.writeFileSync(lineagePath, JSON.stringify(lineage, null, 2)); +console.log(JSON.stringify({ + status: lineage.status, + revision: lineage.revision, + failed_attempts: lineage.correction_slices["S-ea4088e8a61de9f8"].failed_attempts, + used_lines: lineage.correction_slices["S-ea4088e8a61de9f8"].used_lines, + remaining, + pending: lineage.pending_correction, + action_before_begin: action, +}, null, 2)); diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/freeze-lineage.js b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/freeze-lineage.js new file mode 100644 index 00000000..a4ad4d16 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/freeze-lineage.js @@ -0,0 +1,115 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const { execFileSync } = require("child_process"); +const { + deriveReviewDimensions, + validateReviewDecision, + validateGeneralistDecision, +} = require("../../../../scripts/lib/review-dimensions.js"); +const { planReviewGate, planLineageGate } = require("../../../../scripts/lib/review-gate-state.js"); +const { startReviewLineage, beginLens } = require("../../../../scripts/lib/review-lineage.js"); + +const DIR = __dirname; +const ROOT = path.resolve(__dirname, "../../../.."); +const packed = JSON.parse(fs.readFileSync(path.join(DIR, "evidence.json"), "utf8")); +const evidence = packed.evidence; +const generalist = { + status: "needs-specialist", + specialists: ["risk", "reliability"], + reason: "signals=design-risk,diff-auth-permission,metadata-runtime;dimensions=risk,reliability", +}; + +const gv = validateGeneralistDecision(generalist); +if (!gv.valid) { + console.error(gv); + process.exit(1); +} + +const decision = deriveReviewDimensions(evidence, generalist); +const dv = validateReviewDecision(decision); +if (!dv.valid) { + console.error(dv); + process.exit(1); +} + +fs.writeFileSync(path.join(DIR, "derived.json"), JSON.stringify(decision, null, 2)); + +const planned = planReviewGate({ + routeGates: ["clarify", "4r-review-gate"], + existingGate: {}, + decision, + validationErrors: [], +}); +fs.writeFileSync(path.join(DIR, "planned-gate.json"), JSON.stringify(planned, null, 2)); + +function sha256Bytes(buf) { + return `sha256:${crypto.createHash("sha256").update(buf).digest("hex")}`; +} + +const diff = fs.readFileSync(path.join(DIR, "unified.diff")); +const paths = packed.paths.slice().sort(); +const fileDigests = {}; +for (const rel of paths) { + fileDigests[rel] = sha256Bytes(fs.readFileSync(path.join(ROOT, rel))); +} +const candidateTree = sha256Bytes(Buffer.from(JSON.stringify(fileDigests))); +const pathsDigest = sha256Bytes(Buffer.from(paths.join("\n"))); +const baseTree = execFileSync("git", ["rev-parse", "HEAD^{tree}"], { cwd: ROOT, encoding: "utf8" }).trim(); + +const candidate = { + projection: "workspace", + base_tree: baseTree, + candidate_tree: candidateTree, + paths, + diff_hash: sha256Bytes(diff), + paths_digest: pathsDigest, + authored_lines: packed.changed_lines.added, + original_changed_lines: packed.changed_lines.total, +}; + +let lineage = startReviewLineage({ + classification: "high-risk", + evidence_fingerprint: evidence.fingerprint, + selected_dimensions: decision.selected_specialists, + candidate, +}); + +const dimensions = ["risk", "reliability", "resilience", "readability"]; +for (const dimension of dimensions) { + lineage = beginLens(lineage, { + dimension, + request_id: `lens-${dimension}-001`, + expected_revision: lineage.revision, + }); +} + +fs.writeFileSync(path.join(DIR, "lineage.json"), JSON.stringify(lineage, null, 2)); +const lineagePlan = planLineageGate({ + lineage, + observed_candidate_id: lineage.current_candidate_id, + downstream_gate: "status", +}); +fs.writeFileSync(path.join(DIR, "planned-lineage.json"), JSON.stringify(lineagePlan, null, 2)); + +console.log( + JSON.stringify( + { + generalist_valid: gv.valid, + decision_valid: dv.valid, + selected: decision.selected_specialists, + depth: decision.depth, + gate_status: planned.status, + dispatch: planned.dispatch, + lineage_id: lineage.lineage_id, + candidate_id: lineage.current_candidate_id, + revision: lineage.revision, + budget: lineage.correction_budget, + next_action: lineagePlan.next_action || lineagePlan, + }, + null, + 2 + ) +); diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/generalist.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/generalist.json new file mode 100644 index 00000000..bba4ade6 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/generalist.json @@ -0,0 +1,5 @@ +{ + "status": "needs-specialist", + "specialists": ["risk", "reliability"], + "reason": "signals=design-risk,diff-auth-permission,metadata-runtime;dimensions=risk,reliability" +} diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-readability-findings.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-readability-findings.json new file mode 100644 index 00000000..ce385186 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-readability-findings.json @@ -0,0 +1,27 @@ +[ + { + "severity": "WARNING", + "summary": "rejectStaleEvidence recomputa evidence_id sustituyendo candidate_id por predecessorId y trata un hit en nodos del grafo previo o en invalidates transitivo como STALE_EVIDENCE. Ese remint-and-lookup no está comentado.", + "acceptance_criteria": "Comentar en rejectStaleEvidence (predecessorBoundId) que el digest se recalcula bajo el candidato predecesor para que copias remintadas de evidencia invalidada fallen cerrado como STALE_EVIDENCE." + }, + { + "severity": "WARNING", + "summary": "resolveEvidenceProvenance falla UNTRUSTED_COLLECTOR si raw tiene la propiedad collector. El JSDoc habla de autoridad de clase, no de por qué un collector en el payload se rechaza en vez de ignorarse.", + "acceptance_criteria": "Comentar el hasOwnProperty(raw, \"collector\") indicando que la identidad del collector solo puede venir del argumento de canal del harness; un campo collector del payload se rechaza, no se ignora ni se fusiona." + }, + { + "severity": "WARNING", + "summary": "El segundo parámetro de evaluateProvenanceSufficiency se llama obligation pero es solo { requireRuntime?: boolean }; los callers pasan un objeto de opciones, no un registro de obligación.", + "acceptance_criteria": "Renombrar el parámetro y el JSDoc a un nombre de options bag (p. ej. options o sufficiencyOptions) para que no se busquen campos de obligación." + }, + { + "severity": "WARNING", + "summary": "walkMustObligations usa hadWeak (matches.length > 0) para elegir INSUFFICIENT_PROVENANCE vs UNFULFILLED_MUST. El nombre no dice «enlazada pero inadmisible» y el split de reason_code no está comentado.", + "acceptance_criteria": "Renombrar hadWeak a un nombre que signifique evidencia enlazada que falló el gate de provenance runtime, y comentar que MUST con matches inadmisibles falla INSUFFICIENT_PROVENANCE y sin matches falla UNFULFILLED_MUST." + }, + { + "severity": "SUGGESTION", + "summary": "hasNonRuntimeExtra baja el veredicto a PASS WITH WARNINGS solo para external-unverified y human-decision. Ni el nombre («Extra») ni un comentario explican por qué esas dos clases advierten y model-reported no entra aquí.", + "acceptance_criteria": "Comentar junto a hasNonRuntimeExtra que external-unverified y human-decision mantienen verificación pasante con PASS WITH WARNINGS, y que model-reported se omite porque no puede satisfacer MUST runtime." + } +] diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-reliability-findings.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-reliability-findings.json new file mode 100644 index 00000000..34cab812 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-reliability-findings.json @@ -0,0 +1,12 @@ +[ + { + "severity": "WARNING", + "summary": "normalizeEvidence no cubre en test las ramas FABRICATED_EVIDENCE de raw no-objeto y de validateInstance (origin ausente o vacío).", + "acceptance_criteria": "Un test de verifyCandidate/normalizeEvidence con collector allowlisted debe devolver ok:false, reason_code FABRICATED_EVIDENCE y sin verification tanto si un item de rawEvidence es null o string, como si origin está omitido o es \"\"." + }, + { + "severity": "WARNING", + "summary": "walkMustObligations no tiene test para el retorno fail-closed de emitAssessment (if (!emitted.ok) return emitted).", + "acceptance_criteria": "walkMustObligations con un MUST no diferido, evidencia runtime ligada al nodo implementador, y role ausente/vacío o policySnapshotId no sha256, debe devolver ok:false y reason_code INVALID_ASSESSMENT, sin assessments ni PASS." + } +] diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-resilience-findings.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-resilience-findings.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-resilience-findings.json @@ -0,0 +1 @@ +[] diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-risk-findings.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-risk-findings.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lens-risk-findings.json @@ -0,0 +1 @@ +[] diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lineage.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lineage.json new file mode 100644 index 00000000..6bd1050a --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/lineage.json @@ -0,0 +1,334 @@ +{ + "schema_version": 1, + "lineage_id": "sha256:a051818ce2bb310c5fa3a29c8a7b730a564dd5b44ff1da238a73089fcce94c02", + "generation": 2, + "predecessor_lineage_id": "sha256:262dda4ab0b3ec0fe60b7db34683c55d3c4d2590fe69282c40197cbc95aacf42", + "recovery": { + "reason": "new-candidate after harness collector migration and remaining advisory closures", + "approval_reference": "k6b-verification-integrity-remediation-bounded-review-001" + }, + "revision": 9, + "status": "approved", + "genesis": { + "candidate": { + "projection": "workspace", + "base_tree": "e844d66c00f1cdb0f0ca3db871c28fa27af59dea", + "candidate_tree": "sha256:74fb5b293aa5c02f58a4e85b1913f7bc67e30f2fb3413d161e64d7b477901143", + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md", + "openspec/changes/k6b-verification-integrity-remediation/design.md", + "openspec/changes/k6b-verification-integrity-remediation/proposal.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/state.yaml", + "openspec/changes/k6b-verification-integrity-remediation/tasks.md", + "openspec/changes/k6b-verification-integrity-remediation/verify-report.md", + "schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-required.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/assurance-graph/v1.schema.json", + "schemas/kernel/contract-claims.json", + "schemas/kernel/manifest.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/contract-checkers/k1-schema-compat.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.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/k1-scope-guard.test.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js", + "scripts/lib/lifecycle-kernel/k1-compat.js" + ], + "diff_hash": "sha256:c3917621221f275588a92b1895b7f588d48b9f6ce605d68c986d85292d76dbd1", + "paths_digest": "sha256:75fb3b54e19f8e9c2cdad0efb1cf77f81e56dc31d5868555fbaaafb0eeb24eb1", + "authored_lines": 2892, + "original_changed_lines": 3003 + }, + "candidate_id": "sha256:e0a36b2ac29cc84cdaf53d31cc9d09c7e54ad5d196238229eb7d379bb358c648", + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md", + "openspec/changes/k6b-verification-integrity-remediation/design.md", + "openspec/changes/k6b-verification-integrity-remediation/proposal.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/state.yaml", + "openspec/changes/k6b-verification-integrity-remediation/tasks.md", + "openspec/changes/k6b-verification-integrity-remediation/verify-report.md", + "schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-required.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/assurance-graph/v1.schema.json", + "schemas/kernel/contract-claims.json", + "schemas/kernel/manifest.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/contract-checkers/k1-schema-compat.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.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/k1-scope-guard.test.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js", + "scripts/lib/lifecycle-kernel/k1-compat.js" + ], + "classification": "high-risk", + "selected_dimensions": [ + "risk", + "reliability", + "resilience", + "readability" + ], + "evidence_fingerprint": "sha256:66b1b1c10af6adfd036aa014032450b530160d81f05f9dae3af5c1cf09ade779", + "original_changed_lines": 3003, + "authored_lines": 2892 + }, + "current_candidate_id": "sha256:e0a36b2ac29cc84cdaf53d31cc9d09c7e54ad5d196238229eb7d379bb358c648", + "current_candidate": { + "projection": "workspace", + "base_tree": "e844d66c00f1cdb0f0ca3db871c28fa27af59dea", + "candidate_tree": "sha256:74fb5b293aa5c02f58a4e85b1913f7bc67e30f2fb3413d161e64d7b477901143", + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md", + "openspec/changes/k6b-verification-integrity-remediation/design.md", + "openspec/changes/k6b-verification-integrity-remediation/proposal.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/state.yaml", + "openspec/changes/k6b-verification-integrity-remediation/tasks.md", + "openspec/changes/k6b-verification-integrity-remediation/verify-report.md", + "schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-required.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/assurance-graph/v1.schema.json", + "schemas/kernel/contract-claims.json", + "schemas/kernel/manifest.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/contract-checkers/k1-schema-compat.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.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/k1-scope-guard.test.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js", + "scripts/lib/lifecycle-kernel/k1-compat.js" + ], + "diff_hash": "sha256:c3917621221f275588a92b1895b7f588d48b9f6ce605d68c986d85292d76dbd1", + "paths_digest": "sha256:75fb3b54e19f8e9c2cdad0efb1cf77f81e56dc31d5868555fbaaafb0eeb24eb1", + "authored_lines": 2892, + "original_changed_lines": 3003 + }, + "lenses": { + "risk": { + "selected": true, + "status": "completed", + "request_id": "lens-risk-001", + "result_digest": "sha256:0fa38eeaca8882e0d194fd5ba5d8378917d98e795204f8f7a9dfdb78f111f3cc", + "result": { + "findings": [] + }, + "operation": null, + "result_request_id": "lens-risk-001" + }, + "reliability": { + "selected": true, + "status": "completed", + "request_id": "lens-reliability-001", + "result_digest": "sha256:391e5014feee3ef6dbc66b2d97bd67c3059f93d2548b47833a5d34f65593b51e", + "result": { + "findings": [ + { + "severity": "WARNING", + "summary": "normalizeEvidence no cubre en test las ramas FABRICATED_EVIDENCE de raw no-objeto y de validateInstance (origin ausente o vacío).", + "acceptance_criteria": "Un test de verifyCandidate/normalizeEvidence con collector allowlisted debe devolver ok:false, reason_code FABRICATED_EVIDENCE y sin verification tanto si un item de rawEvidence es null o string, como si origin está omitido o es \"\"." + }, + { + "severity": "WARNING", + "summary": "walkMustObligations no tiene test para el retorno fail-closed de emitAssessment (if (!emitted.ok) return emitted).", + "acceptance_criteria": "walkMustObligations con un MUST no diferido, evidencia runtime ligada al nodo implementador, y role ausente/vacío o policySnapshotId no sha256, debe devolver ok:false y reason_code INVALID_ASSESSMENT, sin assessments ni PASS." + } + ] + }, + "operation": null, + "result_request_id": "lens-reliability-001" + }, + "resilience": { + "selected": true, + "status": "completed", + "request_id": "lens-resilience-001", + "result_digest": "sha256:0fa38eeaca8882e0d194fd5ba5d8378917d98e795204f8f7a9dfdb78f111f3cc", + "result": { + "findings": [] + }, + "operation": null, + "result_request_id": "lens-resilience-001" + }, + "readability": { + "selected": true, + "status": "completed", + "request_id": "lens-readability-001", + "result_digest": "sha256:c2a2a061256e9bc2159165fbf9570e8fd3d51b98f5e87ac14554a306e78c2bcc", + "result": { + "findings": [ + { + "severity": "WARNING", + "summary": "rejectStaleEvidence recomputa evidence_id sustituyendo candidate_id por predecessorId y trata un hit en nodos del grafo previo o en invalidates transitivo como STALE_EVIDENCE. Ese remint-and-lookup no está comentado.", + "acceptance_criteria": "Comentar en rejectStaleEvidence (predecessorBoundId) que el digest se recalcula bajo el candidato predecesor para que copias remintadas de evidencia invalidada fallen cerrado como STALE_EVIDENCE." + }, + { + "severity": "WARNING", + "summary": "resolveEvidenceProvenance falla UNTRUSTED_COLLECTOR si raw tiene la propiedad collector. El JSDoc habla de autoridad de clase, no de por qué un collector en el payload se rechaza en vez de ignorarse.", + "acceptance_criteria": "Comentar el hasOwnProperty(raw, \"collector\") indicando que la identidad del collector solo puede venir del argumento de canal del harness; un campo collector del payload se rechaza, no se ignora ni se fusiona." + }, + { + "severity": "WARNING", + "summary": "El segundo parámetro de evaluateProvenanceSufficiency se llama obligation pero es solo { requireRuntime?: boolean }; los callers pasan un objeto de opciones, no un registro de obligación.", + "acceptance_criteria": "Renombrar el parámetro y el JSDoc a un nombre de options bag (p. ej. options o sufficiencyOptions) para que no se busquen campos de obligación." + }, + { + "severity": "WARNING", + "summary": "walkMustObligations usa hadWeak (matches.length > 0) para elegir INSUFFICIENT_PROVENANCE vs UNFULFILLED_MUST. El nombre no dice «enlazada pero inadmisible» y el split de reason_code no está comentado.", + "acceptance_criteria": "Renombrar hadWeak a un nombre que signifique evidencia enlazada que falló el gate de provenance runtime, y comentar que MUST con matches inadmisibles falla INSUFFICIENT_PROVENANCE y sin matches falla UNFULFILLED_MUST." + }, + { + "severity": "SUGGESTION", + "summary": "hasNonRuntimeExtra baja el veredicto a PASS WITH WARNINGS solo para external-unverified y human-decision. Ni el nombre («Extra») ni un comentario explican por qué esas dos clases advierten y model-reported no entra aquí.", + "acceptance_criteria": "Comentar junto a hasNonRuntimeExtra que external-unverified y human-decision mantienen verificación pasante con PASS WITH WARNINGS, y que model-reported se omite porque no puede satisfacer MUST runtime." + } + ] + }, + "operation": null, + "result_request_id": "lens-readability-001" + } + }, + "findings": [ + { + "id": "F-3a0bbdfae84c03f9", + "owner": "reliability", + "severity": "WARNING", + "summary": "normalizeEvidence no cubre en test las ramas FABRICATED_EVIDENCE de raw no-objeto y de validateInstance (origin ausente o vacío).", + "acceptance_criteria": "Un test de verifyCandidate/normalizeEvidence con collector allowlisted debe devolver ok:false, reason_code FABRICATED_EVIDENCE y sin verification tanto si un item de rawEvidence es null o string, como si origin está omitido o es \"\".", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-088ad7080cedccec", + "owner": "reliability", + "severity": "WARNING", + "summary": "walkMustObligations no tiene test para el retorno fail-closed de emitAssessment (if (!emitted.ok) return emitted).", + "acceptance_criteria": "walkMustObligations con un MUST no diferido, evidencia runtime ligada al nodo implementador, y role ausente/vacío o policySnapshotId no sha256, debe devolver ok:false y reason_code INVALID_ASSESSMENT, sin assessments ni PASS.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-f5fce295fe3508ce", + "owner": "readability", + "severity": "WARNING", + "summary": "rejectStaleEvidence recomputa evidence_id sustituyendo candidate_id por predecessorId y trata un hit en nodos del grafo previo o en invalidates transitivo como STALE_EVIDENCE. Ese remint-and-lookup no está comentado.", + "acceptance_criteria": "Comentar en rejectStaleEvidence (predecessorBoundId) que el digest se recalcula bajo el candidato predecesor para que copias remintadas de evidencia invalidada fallen cerrado como STALE_EVIDENCE.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-1533f335412c012a", + "owner": "readability", + "severity": "WARNING", + "summary": "resolveEvidenceProvenance falla UNTRUSTED_COLLECTOR si raw tiene la propiedad collector. El JSDoc habla de autoridad de clase, no de por qué un collector en el payload se rechaza en vez de ignorarse.", + "acceptance_criteria": "Comentar el hasOwnProperty(raw, \"collector\") indicando que la identidad del collector solo puede venir del argumento de canal del harness; un campo collector del payload se rechaza, no se ignora ni se fusiona.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-3a01c03d9d2629ba", + "owner": "readability", + "severity": "WARNING", + "summary": "El segundo parámetro de evaluateProvenanceSufficiency se llama obligation pero es solo { requireRuntime?: boolean }; los callers pasan un objeto de opciones, no un registro de obligación.", + "acceptance_criteria": "Renombrar el parámetro y el JSDoc a un nombre de options bag (p. ej. options o sufficiencyOptions) para que no se busquen campos de obligación.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-15a83b999026fe5c", + "owner": "readability", + "severity": "WARNING", + "summary": "walkMustObligations usa hadWeak (matches.length > 0) para elegir INSUFFICIENT_PROVENANCE vs UNFULFILLED_MUST. El nombre no dice «enlazada pero inadmisible» y el split de reason_code no está comentado.", + "acceptance_criteria": "Renombrar hadWeak a un nombre que signifique evidencia enlazada que falló el gate de provenance runtime, y comentar que MUST con matches inadmisibles falla INSUFFICIENT_PROVENANCE y sin matches falla UNFULFILLED_MUST.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-268a28557abf86de", + "owner": "readability", + "severity": "SUGGESTION", + "summary": "hasNonRuntimeExtra baja el veredicto a PASS WITH WARNINGS solo para external-unverified y human-decision. Ni el nombre («Extra») ni un comentario explican por qué esas dos clases advierten y model-reported no entra aquí.", + "acceptance_criteria": "Comentar junto a hasNonRuntimeExtra que external-unverified y human-decision mantienen verificación pasante con PASS WITH WARNINGS, y que model-reported se omite porque no puede satisfacer MUST runtime.", + "blocking": false, + "resolution": "advisory" + } + ], + "findings_digest": "sha256:f64e728a7b86e4ac9019e6a4fb9fcfe9c5b8720786c49a5599d8b6f79ffd35cc", + "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" +} \ No newline at end of file diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/manifest-v2.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/manifest-v2.json new file mode 100644 index 00000000..e1dd772a --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/manifest-v2.json @@ -0,0 +1,34 @@ +{ + "slices": [ + { + "root_cause_key": "collector-trust-boundary", + "finding_ids": [ + "F-d5739d79237afeb8" + ], + "permitted_paths": [ + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "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/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.js", + "scripts/lib/independent-verifier/index.test.js" + ] + }, + { + "root_cause_key": "must-walk-coverage", + "finding_ids": [ + "F-b3d6518c12aa69fe", + "F-00f97ff647d28eea", + "F-ef73f7e16cab6436" + ], + "permitted_paths": [ + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "scripts/lib/independent-verifier/obligation-coverage.js", + "scripts/lib/independent-verifier/obligation-coverage.test.js" + ] + } + ] +} \ No newline at end of file diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/migrate-and-begin-slice1.js b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/migrate-and-begin-slice1.js new file mode 100644 index 00000000..6d78d6e5 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/migrate-and-begin-slice1.js @@ -0,0 +1,69 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const { migrateReviewLineage, beginCorrection, nextLineageAction } = require("../../../../scripts/lib/review-lineage.js"); + +const dir = __dirname; +const lineagePath = path.join(dir, "lineage.json"); +let lineage = JSON.parse(fs.readFileSync(lineagePath, "utf8")); + +const slice1Paths = [ + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "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/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.js", + "scripts/lib/independent-verifier/index.test.js", +]; +const slice2Paths = [ + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "scripts/lib/independent-verifier/obligation-coverage.js", + "scripts/lib/independent-verifier/obligation-coverage.test.js", +]; + +const manifest = { + slices: [ + { + root_cause_key: "collector-trust-boundary", + finding_ids: ["F-d5739d79237afeb8"], + permitted_paths: slice1Paths, + }, + { + root_cause_key: "must-walk-coverage", + finding_ids: ["F-b3d6518c12aa69fe", "F-00f97ff647d28eea", "F-ef73f7e16cab6436"], + permitted_paths: slice2Paths, + }, + ], +}; + +lineage = migrateReviewLineage(lineage, manifest); +fs.writeFileSync(path.join(dir, "manifest-v2.json"), JSON.stringify(manifest, null, 2)); + +const action = nextLineageAction(lineage); +const sliceId = action.slice_id; +const slice = lineage.correction_slices[sliceId]; + +lineage = beginCorrection(lineage, { + request_id: "slice-1-correction-start", + expected_revision: lineage.revision, + slice_id: sliceId, + finding_ids: slice.finding_ids, + paths: slice.permitted_paths, + base_candidate_id: lineage.current_candidate_id, + forecast_lines: 180, +}); + +fs.writeFileSync(lineagePath, JSON.stringify(lineage, null, 2)); +console.log(JSON.stringify({ + status: lineage.status, + revision: lineage.revision, + active_slice_id: lineage.active_slice_id, + slice_order: lineage.slice_order, + slices: Object.fromEntries(Object.entries(lineage.correction_slices).map(([id, s]) => [id, { root_cause_key: s.root_cause_key, finding_ids: s.finding_ids, status: s.status, limit_lines: s.limit_lines }])), + pending_correction: lineage.pending_correction, + next_before_begin: action, +}, null, 2)); diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/normalize-evidence.js b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/normalize-evidence.js new file mode 100644 index 00000000..d4760981 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/normalize-evidence.js @@ -0,0 +1,145 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const { execFileSync } = require("child_process"); +const { normalizeReviewEvidence } = require("../../../../scripts/lib/review-dimensions.js"); + +const ROOT = path.resolve(__dirname, "../../../.."); +const OUT_DIR = __dirname; +const EXCLUDE = new Set(["models.yaml"]); +const EXCLUDE_PREFIX = "openspec/changes/k6b-verification-integrity-remediation/.4r/"; + +function posix(p) { + return p.replace(/\\/g, "/"); +} + +function listFilesRecursive(dir) { + const out = []; + for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { + const abs = path.join(dir, ent.name); + if (ent.isDirectory()) out.push(...listFilesRecursive(abs)); + else out.push(abs); + } + return out; +} + +function gitStatus() { + const status = execFileSync("git", ["status", "--short", "-u"], { + cwd: ROOT, + encoding: "utf8", + }); + const files = []; + for (const line of status.split(/\r?\n/).filter(Boolean)) { + const raw = line.slice(3); + const name = raw.includes(" -> ") ? raw.split(" -> ").pop() : raw; + if (EXCLUDE.has(name) || name.startsWith(EXCLUDE_PREFIX)) continue; + files.push({ status: line.slice(0, 2), name }); + } + const expanded = []; + for (const f of files) { + const abs = path.join(ROOT, f.name); + if (f.status.includes("?") && fs.existsSync(abs) && fs.statSync(abs).isDirectory()) { + for (const p of listFilesRecursive(abs)) { + const rel = posix(path.relative(ROOT, p)); + if (rel.startsWith(EXCLUDE_PREFIX)) continue; + expanded.push({ status: "??", name: rel }); + } + } else { + expanded.push({ ...f, name: posix(f.name) }); + } + } + const unique = [...new Map(expanded.map((f) => [f.name, f])).values()]; + unique.sort((a, b) => a.name.localeCompare(b.name)); + return unique; +} + +function syntheticDiff(rel) { + const abs = path.join(ROOT, rel); + const text = fs.readFileSync(abs, "utf8").replace(/\r\n/g, "\n"); + const body = text.endsWith("\n") ? text.slice(0, -1) : text; + const lines = body.length ? body.split("\n") : []; + const hunkCount = Math.max(lines.length, 1); + const parts = [ + `diff --git a/${rel} b/${rel}`, + "new file mode 100644", + "--- /dev/null", + `+++ b/${rel}`, + `@@ -0,0 +1,${lines.length || 1} @@`, + ]; + if (lines.length === 0) parts.push("+"); + else parts.push(...lines.map((l) => `+${l}`)); + return `${parts.join("\n")}\n`; +} + +const unique = gitStatus(); +const modified = unique.filter((f) => !f.status.includes("?")); +const untracked = unique.filter((f) => f.status.includes("?")); +const sections = []; + +if (modified.length) { + const tracked = execFileSync( + "git", + ["diff", "--no-color", "HEAD", "--", ...modified.map((f) => f.name)], + { cwd: ROOT, encoding: "utf8", maxBuffer: 50 * 1024 * 1024 } + ).replace(/\r\n/g, "\n"); + if (tracked.trim()) sections.push(tracked.endsWith("\n") ? tracked : `${tracked}\n`); +} +for (const f of untracked) { + sections.push(syntheticDiff(f.name)); +} + +const diff = sections.join(""); +fs.writeFileSync(path.join(OUT_DIR, "unified.diff"), diff); + +const paths = unique.map((f) => f.name); +const operationTypes = []; +if (modified.length) operationTypes.push("modify"); +if (untracked.length) operationTypes.push("add"); + +const evidence = normalizeReviewEvidence({ + classification: "high-risk", + verify: { status: "success", findings: [] }, + diff, + paths, + capabilities: ["runtime"], + dependencies: [], + operationTypes, + designRisks: [{ code: "design-risk", detail: "verification-integrity-trust-boundary" }], +}); + +const added = (diff.match(/^\+/gm) || []).filter((l) => !l.startsWith("+++")).length; +const removed = (diff.match(/^-/gm) || []).filter((l) => !l.startsWith("---")).length; + +fs.writeFileSync( + path.join(OUT_DIR, "evidence.json"), + JSON.stringify( + { + evidence, + paths, + operationTypes, + changed_lines: { added, removed, total: added + removed }, + file_count: paths.length, + facts: evidence.sources.facts, + }, + null, + 2 + ) +); + +console.log( + JSON.stringify( + { + files: paths.length, + added, + removed, + total: added + removed, + fingerprint: evidence.fingerprint, + facts: evidence.sources.facts.map((f) => f.code), + operationTypes, + diffBytes: Buffer.byteLength(diff), + }, + null, + 2 + ) +); diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/pass-slice1-begin-slice2.js b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/pass-slice1-begin-slice2.js new file mode 100644 index 00000000..e80bde16 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/pass-slice1-begin-slice2.js @@ -0,0 +1,66 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const { + applyTargetedValidation, + beginCorrection, + nextLineageAction, +} = require("../../../../scripts/lib/review-lineage.js"); + +const lineagePath = path.join(__dirname, "lineage.json"); +let lineage = JSON.parse(fs.readFileSync(lineagePath, "utf8")); + +lineage = applyTargetedValidation(lineage, { + request_id: "slice-1-validation-002", + expected_revision: lineage.revision, + slice_id: "S-ea4088e8a61de9f8", + outcomes: [{ id: "F-d5739d79237afeb8", status: "resolved" }], + regression: { + detected: false, + evidence: [ + "static: resolveEvidenceProvenance(raw, harnessCollector) returns UNTRUSTED_COLLECTOR if raw.collector exists; deriveProvenanceClass uses only harnessCollector; trusted mismatch (including weak vs allowlisted) fails closed", + "node --test --test-name-pattern \"envelope collector fails closed\" scripts/lib/independent-verifier/index.test.js → GREEN (apply-progress Batch 3)", + "index.test.js 10 pass / 25 fail because raw() still attaches collector to the envelope; no passed slices exist", + ], + impacted_slices: [], + }, + follow_ups: [ + { + owner: "reliability", + summary: "raw(), e2e y assurance-graph tests siguen inyectando collector en el sobre del worker; index.test.js queda 10/25 y el camino harness no cubre claims débiles en el test focal.", + }, + { + owner: "risk", + summary: "input.collector worker con claim human-decision|external-unverified se reescribe a model-reported sin UNTRUSTED_COLLECTOR; no escala a clase fuerte.", + }, + ], +}); + +const action = nextLineageAction(lineage); +if (action.type !== "correct" || action.slice_id !== "S-ad5558b5639b6890") { + fs.writeFileSync(lineagePath, JSON.stringify(lineage, null, 2)); + console.log(JSON.stringify({ status: lineage.status, action, slice1: lineage.correction_slices["S-ea4088e8a61de9f8"].status }, null, 2)); + process.exit(1); +} + +const slice = lineage.correction_slices[action.slice_id]; +lineage = beginCorrection(lineage, { + request_id: "slice-2-correction-start", + expected_revision: lineage.revision, + slice_id: action.slice_id, + finding_ids: slice.finding_ids, + paths: slice.permitted_paths, + base_candidate_id: lineage.current_candidate_id, + forecast_lines: 130, +}); + +fs.writeFileSync(lineagePath, JSON.stringify(lineage, null, 2)); +console.log(JSON.stringify({ + status: lineage.status, + revision: lineage.revision, + slice1: lineage.correction_slices["S-ea4088e8a61de9f8"].status, + slice2: lineage.correction_slices["S-ad5558b5639b6890"].status, + pending: lineage.pending_correction, + follow_ups: lineage.follow_ups.length, +}, null, 2)); diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/pass-slice2.js b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/pass-slice2.js new file mode 100644 index 00000000..6fe3f48d --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/pass-slice2.js @@ -0,0 +1,46 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const { applyTargetedValidation, nextLineageAction } = require("../../../../scripts/lib/review-lineage.js"); + +const lineagePath = path.join(__dirname, "lineage.json"); +let lineage = JSON.parse(fs.readFileSync(lineagePath, "utf8")); + +lineage = applyTargetedValidation(lineage, { + request_id: "slice-2-validation-001", + expected_revision: lineage.revision, + slice_id: "S-ad5558b5639b6890", + outcomes: [ + { id: "F-00f97ff647d28eea", status: "resolved" }, + { id: "F-b3d6518c12aa69fe", status: "resolved" }, + { id: "F-ef73f7e16cab6436", status: "resolved" }, + ], + regression: { + detected: false, + evidence: [ + "static: obligation-coverage.js only tightened missing/non-array graph to BINDING_MISMATCH and UNFULFILLED_MUST wording (evidence, not assessment); collector-trust-boundary paths were not in this delta", + "node --test scripts/lib/independent-verifier/obligation-coverage.test.js → 10 pass / 0 fail; no attributable break of S-ea4088e8a61de9f8 / F-d5739d79237afeb8", + ], + impacted_slices: [], + }, + follow_ups: [ + { + owner: "readability", + summary: "F-f979f00ae92cda6f sigue advisory: required_evidence solo comprueba vacío; el matching no usa su contenido.", + }, + ], +}); + +const action = nextLineageAction(lineage); +fs.writeFileSync(lineagePath, JSON.stringify(lineage, null, 2)); +console.log(JSON.stringify({ + status: lineage.status, + revision: lineage.revision, + terminal_reason: lineage.terminal_reason, + slice1: lineage.correction_slices["S-ea4088e8a61de9f8"].status, + slice2: lineage.correction_slices["S-ad5558b5639b6890"].status, + blocking: lineage.findings.filter((f) => f.blocking).map((f) => ({ id: f.id, resolution: f.resolution })), + follow_ups: lineage.follow_ups.length, + action, +}, null, 2)); diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/persist-gate.js b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/persist-gate.js new file mode 100644 index 00000000..2af95047 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/persist-gate.js @@ -0,0 +1,48 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +const dir = __dirname; +const yamlPath = path.resolve(__dirname, "../state.yaml"); +const planned = JSON.parse(fs.readFileSync(path.join(dir, "planned-gate.json"), "utf8")); +const lineage = JSON.parse(fs.readFileSync(path.join(dir, "lineage.json"), "utf8")); +const generalist = { + status: "needs-specialist", + specialists: ["risk", "reliability"], + reason: "signals=design-risk,diff-auth-permission,metadata-runtime;dimensions=risk,reliability", +}; +fs.writeFileSync(path.join(dir, "generalist.json"), JSON.stringify(generalist, null, 2)); + +let text = fs.readFileSync(yamlPath, "utf8"); +text = text.replace(/last_updated: ".*"/, 'last_updated: "2026-08-27T15:30:00Z"'); +if (!/\ngates:/.test(text)) { + text += [ + "", + "gates:", + " 4r-review-gate:", + " status: ready", + " schema_version: 1", + " classification: high-risk", + " depth:", + " review: strict", + " lineage_status: reviewing", + ` lineage_id: "${lineage.lineage_id}"`, + ` lineage_revision: ${lineage.revision}`, + " lineage_artifact: openspec/changes/k6b-verification-integrity-remediation/.4r/lineage.json", + " archive_allowed: false", + " selected_specialists: [risk, reliability, resilience, readability]", + " evidence:", + " schema_version: 1", + ` fingerprint: "${planned.gate.evidence.fingerprint}"`, + " generalist:", + " status: needs-specialist", + " specialists: [risk, reliability]", + ` reason: "${generalist.reason}"`, + "", + ].join("\n"); + fs.writeFileSync(yamlPath, text); + console.log("appended 4r-review-gate"); +} else { + console.log("gates already present"); +} diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/planned-gate.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/planned-gate.json new file mode 100644 index 00000000..c011a435 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/planned-gate.json @@ -0,0 +1,178 @@ +{ + "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:66b1b1c10af6adfd036aa014032450b530160d81f05f9dae3af5c1cf09ade779", + "sources": { + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md", + "openspec/changes/k6b-verification-integrity-remediation/design.md", + "openspec/changes/k6b-verification-integrity-remediation/proposal.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/state.yaml", + "openspec/changes/k6b-verification-integrity-remediation/tasks.md", + "openspec/changes/k6b-verification-integrity-remediation/verify-report.md", + "schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-required.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/assurance-graph/v1.schema.json", + "schemas/kernel/contract-claims.json", + "schemas/kernel/manifest.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/contract-checkers/k1-schema-compat.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.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/k1-scope-guard.test.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js", + "scripts/lib/lifecycle-kernel/k1-compat.js" + ], + "capabilities": [ + "runtime" + ], + "operation_types": [ + "add", + "modify" + ], + "dependencies": [], + "facts": [ + { + "code": "design-risk", + "source": "design", + "detail": "verification-integrity-trust-boundary" + }, + { + "code": "metadata-runtime", + "source": "metadata", + "detail": "runtime" + }, + { + "code": "diff-auth-permission", + "source": "real-diff", + "detail": "scripts/lib/independent-verifier/assessment.js,scripts/lib/independent-verifier/obligation-coverage.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/assessment.js,scripts/lib/independent-verifier/obligation-coverage.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": "verification-integrity-trust-boundary", + "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 + } + ] + } + } + } +} \ No newline at end of file diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/planned-lineage.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/planned-lineage.json new file mode 100644 index 00000000..8c1347f1 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/planned-lineage.json @@ -0,0 +1,14 @@ +{ + "status": "reviewing", + "next_action": { + "type": "await-lenses", + "dimensions": [ + "risk", + "reliability", + "resilience", + "readability" + ] + }, + "dispatch": [], + "archive_allowed": false +} \ No newline at end of file diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/predecessor-lineage.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/predecessor-lineage.json new file mode 100644 index 00000000..83132360 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/predecessor-lineage.json @@ -0,0 +1,661 @@ +{ + "schema_version": 1, + "lineage_id": "sha256:262dda4ab0b3ec0fe60b7db34683c55d3c4d2590fe69282c40197cbc95aacf42", + "generation": 1, + "predecessor_lineage_id": null, + "recovery": null, + "revision": 18, + "status": "approved", + "genesis": { + "candidate": { + "projection": "workspace", + "base_tree": "e844d66c00f1cdb0f0ca3db871c28fa27af59dea", + "candidate_tree": "sha256:9f5517b71f4ef17351e5c4a7a37c797e91ee9200473dc75fd0be507431e87afe", + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md", + "openspec/changes/k6b-verification-integrity-remediation/design.md", + "openspec/changes/k6b-verification-integrity-remediation/proposal.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/state.yaml", + "openspec/changes/k6b-verification-integrity-remediation/tasks.md", + "openspec/changes/k6b-verification-integrity-remediation/verify-report.md", + "schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-required.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/assurance-graph/v1.schema.json", + "schemas/kernel/contract-claims.json", + "schemas/kernel/manifest.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/contract-checkers/k1-schema-compat.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.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/k1-scope-guard.test.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js", + "scripts/lib/lifecycle-kernel/k1-compat.js" + ], + "diff_hash": "sha256:2221fd5e68f774e1b74758c595e9d2cb948ffa11b1e25c2a1496bf53367d2a42", + "paths_digest": "sha256:75fb3b54e19f8e9c2cdad0efb1cf77f81e56dc31d5868555fbaaafb0eeb24eb1", + "authored_lines": 2584, + "original_changed_lines": 2697 + }, + "candidate_id": "sha256:89e60d916894d18b590e430d48c2be9afeb1c22fb1286ee3548d43be3e8c1a33", + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md", + "openspec/changes/k6b-verification-integrity-remediation/design.md", + "openspec/changes/k6b-verification-integrity-remediation/proposal.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/state.yaml", + "openspec/changes/k6b-verification-integrity-remediation/tasks.md", + "openspec/changes/k6b-verification-integrity-remediation/verify-report.md", + "schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-required.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/assurance-graph/v1.schema.json", + "schemas/kernel/contract-claims.json", + "schemas/kernel/manifest.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/contract-checkers/k1-schema-compat.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.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/k1-scope-guard.test.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js", + "scripts/lib/lifecycle-kernel/k1-compat.js" + ], + "classification": "high-risk", + "selected_dimensions": [ + "risk", + "reliability", + "resilience", + "readability" + ], + "evidence_fingerprint": "sha256:66b1b1c10af6adfd036aa014032450b530160d81f05f9dae3af5c1cf09ade779", + "original_changed_lines": 2697, + "authored_lines": 2584 + }, + "current_candidate_id": "sha256:e3b5973eff9d265352006b40009a7a349da73588f1e24777e15d1f12c3b71424", + "current_candidate": { + "projection": "workspace", + "base_tree": "e844d66c00f1cdb0f0ca3db871c28fa27af59dea", + "candidate_tree": "sha256:1dfdc248553c545ceda26f91499498658ba9727b889273bd10f43801c3a0c234", + "paths": [ + "docs/architecture/harness-evolution.md", + "docs/roadmaps/harness-evolution.md", + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md", + "openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md", + "openspec/changes/k6b-verification-integrity-remediation/design.md", + "openspec/changes/k6b-verification-integrity-remediation/proposal.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md", + "openspec/changes/k6b-verification-integrity-remediation/state.yaml", + "openspec/changes/k6b-verification-integrity-remediation/tasks.md", + "openspec/changes/k6b-verification-integrity-remediation/verify-report.md", + "schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json", + "schemas/kernel/assessment/fixtures/invalid/v1-missing-required.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/assurance-graph/v1.schema.json", + "schemas/kernel/contract-claims.json", + "schemas/kernel/manifest.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/contract-checkers/k1-schema-compat.js", + "scripts/lib/independent-verifier/assessment.js", + "scripts/lib/independent-verifier/assessment.test.js", + "scripts/lib/independent-verifier/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.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/k1-scope-guard.test.js", + "scripts/lib/k2a-maturity-docs.test.js", + "scripts/lib/k6b-schema-fixtures.test.js", + "scripts/lib/lifecycle-kernel/k1-compat.js" + ], + "diff_hash": "sha256:2221fd5e68f774e1b74758c595e9d2cb948ffa11b1e25c2a1496bf53367d2a42", + "paths_digest": "sha256:75fb3b54e19f8e9c2cdad0efb1cf77f81e56dc31d5868555fbaaafb0eeb24eb1", + "authored_lines": 2584, + "original_changed_lines": 2697 + }, + "lenses": { + "risk": { + "selected": true, + "status": "completed", + "request_id": "lens-risk-001", + "result_digest": "sha256:228a8c1d1bc1fe6ee92515fe50751606682399385b0fc021016e95a2f546d2de", + "result": { + "findings": [ + { + "severity": "CRITICAL", + "summary": "Collector allowlisteado en el mismo raw que la observación reclasifica claims débiles a clase fuerte y puede satisfacer MUST runtime.", + "acceptance_criteria": "Un sobre con provenance model-reported|human-decision|external-unverified y collector allowlisteado MUST fallar cerrado. Desacuerdo claim↔collector MUST ser simétrico. Collector de harness MUST no vivir en el mismo objeto que bytes/origin del worker." + }, + { + "severity": "WARNING", + "summary": "STALE_EVIDENCE es opt-in y compara evidence_id reacuñado con candidate_id; copiar bytes del predecesor al sucesor elude stale y foreign.", + "acceptance_criteria": "Con candidato que declara predecesor, omitir priorAssuranceGraph MUST no permitir PASS sobre evidencia del predecesor. Reusar digest/bytes bajo invalidates transitivo MUST ser STALE_EVIDENCE aunque el evidence_id nuevo difiera." + }, + { + "severity": "WARNING", + "summary": "canonicalInputs caller-supplied se fingerprintan en graph_id sin igualarlos al Execution Graph o policy ya bindeados.", + "acceptance_criteria": "Si canonicalInputs.contract_digest, policy_snapshot_id o execution_graph_digest no coinciden con el grafo/contrato bindeado, verifyCandidate MUST fallar cerrado y MUST no emitir PASS." + } + ] + }, + "operation": null, + "result_request_id": "lens-risk-001" + }, + "reliability": { + "selected": true, + "status": "completed", + "request_id": "lens-reliability-001", + "result_digest": "sha256:44903861240df9e8fb36c4ae0c9f8697521539015ed27b57176c8c613347969c", + "result": { + "findings": [ + { + "severity": "CRITICAL", + "summary": "walkMustObligations no tiene test para matches con proveniencia débil (INSUFFICIENT_PROVENANCE).", + "acceptance_criteria": "Un test con strategy mínima satisfecha por evidencia runtime no ligada al MUST, y evidencia débil sí ligada, debe devolver ok:false, reason_code INSUFFICIENT_PROVENANCE, identificar el obligation_id y no emitir verification." + }, + { + "severity": "CRITICAL", + "summary": "El walk MUST solo se prueba con una obligación; un segundo MUST incumplido no está cubierto.", + "acceptance_criteria": "Grafo con dos MUST no diferidos; evidencia admisible solo para el primero; el walk o la fachada falla UNFULFILLED_MUST identificando el segundo obligation_id y no emite PASS." + }, + { + "severity": "CRITICAL", + "summary": "Un deferral parcial (reason sin approved_by, o al revés) no tiene test de que el MUST sigue exigiendo cobertura.", + "acceptance_criteria": "MUST con deferral incompleto (falta approved_by, o reason/approved_by solo whitespace) y sin evidencia admisible → UNFULFILLED_MUST, no PASS." + }, + { + "severity": "WARNING", + "summary": "Faltan tests del mapeo fail-closed de collectors: desacuerdo de clase, transporte incorrecto e ids npm-test/node:test.", + "acceptance_criteria": "Claim strong distinto de la clase derivada → UNTRUSTED_COLLECTOR; id allowlisted con transport no listado → UNTRUSTED_COLLECTOR; npm-test y node:test derivan runtime-observed." + }, + { + "severity": "WARNING", + "summary": "La rama GRAPH_DIVERGENCE de mapProjectionFailure/verifyCandidate no tiene test.", + "acceptance_criteria": "Un test que fuerce verifyCandidate a ok:false, reason_code GRAPH_DIVERGENCE, sin verification ni assurance_graph." + }, + { + "severity": "WARNING", + "summary": "walkMustObligations sin executionGraph (o con obligations no-array) devuelve ok:true y assessments vacíos.", + "acceptance_criteria": "walkMustObligations sin grafo compilado, o con obligations no-array, falla cerrado (no ok:true vacío)." + } + ] + }, + "operation": null, + "result_request_id": "lens-reliability-001" + }, + "resilience": { + "selected": true, + "status": "completed", + "request_id": "lens-resilience-001", + "result_digest": "sha256:0fa38eeaca8882e0d194fd5ba5d8378917d98e795204f8f7a9dfdb78f111f3cc", + "result": { + "findings": [] + }, + "operation": null, + "result_request_id": "lens-resilience-001" + }, + "readability": { + "selected": true, + "status": "completed", + "request_id": "lens-readability-001", + "result_digest": "sha256:6819bfc4e19db3fdf87db5f818cc79154e92990b82783d7d50fa0834d462cdf4", + "result": { + "findings": [ + { + "severity": "WARNING", + "summary": "rejectForbidden usa FORBIDDEN_RELATION para kind, namespace y relation; FORBIDDEN_KINDS es inalcanzable tras el allowlist.", + "acceptance_criteria": "Distinguir fallos de kind, namespace y relation; eliminar FORBIDDEN_KINDS si el allowlist es la autoridad de kind, o documentar defensa en profundidad." + }, + { + "severity": "WARNING", + "summary": "UNFULFILLED_MUST dice «no admissible assessment» cuando el hueco es de evidencia, antes de emitAssessment.", + "acceptance_criteria": "El mensaje debe nombrar evidencia o provenance ausente, no assessment, hasta que exista un registro assessment/v1." + }, + { + "severity": "WARNING", + "summary": "El projector materializa assessments como aristas satisfies evidencia→obligación, sin nodo assessment ni comentario de que role/assessment_id se colapsan.", + "acceptance_criteria": "Comentar que satisfies sale de assessments persistibles; assessment no es nodo; roles distintos del mismo par colapsan a una arista." + }, + { + "severity": "WARNING", + "summary": "derived nombra a la vez la clase del allowlist y el objeto collector; un collector trusted pisa provenance débil reclamada sin comentario.", + "acceptance_criteria": "Renombrar campo vs variable y documentar que el collector allowlisted es la autoridad de la clase almacenada, también frente a claims débiles." + }, + { + "severity": "SUGGESTION", + "summary": "required_evidence se carga solo para comprobar vacío; el matching no usa su contenido.", + "acceptance_criteria": "Comentar que el contrato es presencia no vacía, no matching por contenido, o usar required_evidence en el emparejamiento." + } + ] + }, + "operation": null, + "result_request_id": "lens-readability-001" + } + }, + "findings": [ + { + "id": "F-d5739d79237afeb8", + "owner": "risk", + "severity": "CRITICAL", + "summary": "Collector allowlisteado en el mismo raw que la observación reclasifica claims débiles a clase fuerte y puede satisfacer MUST runtime.", + "acceptance_criteria": "Un sobre con provenance model-reported|human-decision|external-unverified y collector allowlisteado MUST fallar cerrado. Desacuerdo claim↔collector MUST ser simétrico. Collector de harness MUST no vivir en el mismo objeto que bytes/origin del worker.", + "blocking": true, + "resolution": "resolved" + }, + { + "id": "F-ad61b7e3cff9629a", + "owner": "risk", + "severity": "WARNING", + "summary": "STALE_EVIDENCE es opt-in y compara evidence_id reacuñado con candidate_id; copiar bytes del predecesor al sucesor elude stale y foreign.", + "acceptance_criteria": "Con candidato que declara predecesor, omitir priorAssuranceGraph MUST no permitir PASS sobre evidencia del predecesor. Reusar digest/bytes bajo invalidates transitivo MUST ser STALE_EVIDENCE aunque el evidence_id nuevo difiera.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-6b1f8c8265c82b3e", + "owner": "risk", + "severity": "WARNING", + "summary": "canonicalInputs caller-supplied se fingerprintan en graph_id sin igualarlos al Execution Graph o policy ya bindeados.", + "acceptance_criteria": "Si canonicalInputs.contract_digest, policy_snapshot_id o execution_graph_digest no coinciden con el grafo/contrato bindeado, verifyCandidate MUST fallar cerrado y MUST no emitir PASS.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-b3d6518c12aa69fe", + "owner": "reliability", + "severity": "CRITICAL", + "summary": "walkMustObligations no tiene test para matches con proveniencia débil (INSUFFICIENT_PROVENANCE).", + "acceptance_criteria": "Un test con strategy mínima satisfecha por evidencia runtime no ligada al MUST, y evidencia débil sí ligada, debe devolver ok:false, reason_code INSUFFICIENT_PROVENANCE, identificar el obligation_id y no emitir verification.", + "blocking": true, + "resolution": "resolved" + }, + { + "id": "F-00f97ff647d28eea", + "owner": "reliability", + "severity": "CRITICAL", + "summary": "El walk MUST solo se prueba con una obligación; un segundo MUST incumplido no está cubierto.", + "acceptance_criteria": "Grafo con dos MUST no diferidos; evidencia admisible solo para el primero; el walk o la fachada falla UNFULFILLED_MUST identificando el segundo obligation_id y no emite PASS.", + "blocking": true, + "resolution": "resolved" + }, + { + "id": "F-ef73f7e16cab6436", + "owner": "reliability", + "severity": "CRITICAL", + "summary": "Un deferral parcial (reason sin approved_by, o al revés) no tiene test de que el MUST sigue exigiendo cobertura.", + "acceptance_criteria": "MUST con deferral incompleto (falta approved_by, o reason/approved_by solo whitespace) y sin evidencia admisible → UNFULFILLED_MUST, no PASS.", + "blocking": true, + "resolution": "resolved" + }, + { + "id": "F-2fc6db350f5b8afc", + "owner": "reliability", + "severity": "WARNING", + "summary": "Faltan tests del mapeo fail-closed de collectors: desacuerdo de clase, transporte incorrecto e ids npm-test/node:test.", + "acceptance_criteria": "Claim strong distinto de la clase derivada → UNTRUSTED_COLLECTOR; id allowlisted con transport no listado → UNTRUSTED_COLLECTOR; npm-test y node:test derivan runtime-observed.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-990aa817913b8273", + "owner": "reliability", + "severity": "WARNING", + "summary": "La rama GRAPH_DIVERGENCE de mapProjectionFailure/verifyCandidate no tiene test.", + "acceptance_criteria": "Un test que fuerce verifyCandidate a ok:false, reason_code GRAPH_DIVERGENCE, sin verification ni assurance_graph.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-2be19c4683d81ba1", + "owner": "reliability", + "severity": "WARNING", + "summary": "walkMustObligations sin executionGraph (o con obligations no-array) devuelve ok:true y assessments vacíos.", + "acceptance_criteria": "walkMustObligations sin grafo compilado, o con obligations no-array, falla cerrado (no ok:true vacío).", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-99f9c70bdae46c12", + "owner": "readability", + "severity": "WARNING", + "summary": "rejectForbidden usa FORBIDDEN_RELATION para kind, namespace y relation; FORBIDDEN_KINDS es inalcanzable tras el allowlist.", + "acceptance_criteria": "Distinguir fallos de kind, namespace y relation; eliminar FORBIDDEN_KINDS si el allowlist es la autoridad de kind, o documentar defensa en profundidad.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-9d6a187e3d18dbf5", + "owner": "readability", + "severity": "WARNING", + "summary": "UNFULFILLED_MUST dice «no admissible assessment» cuando el hueco es de evidencia, antes de emitAssessment.", + "acceptance_criteria": "El mensaje debe nombrar evidencia o provenance ausente, no assessment, hasta que exista un registro assessment/v1.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-eb2d325d6d801a14", + "owner": "readability", + "severity": "WARNING", + "summary": "El projector materializa assessments como aristas satisfies evidencia→obligación, sin nodo assessment ni comentario de que role/assessment_id se colapsan.", + "acceptance_criteria": "Comentar que satisfies sale de assessments persistibles; assessment no es nodo; roles distintos del mismo par colapsan a una arista.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-4839a9a36f0b55be", + "owner": "readability", + "severity": "WARNING", + "summary": "derived nombra a la vez la clase del allowlist y el objeto collector; un collector trusted pisa provenance débil reclamada sin comentario.", + "acceptance_criteria": "Renombrar campo vs variable y documentar que el collector allowlisted es la autoridad de la clase almacenada, también frente a claims débiles.", + "blocking": false, + "resolution": "advisory" + }, + { + "id": "F-f979f00ae92cda6f", + "owner": "readability", + "severity": "SUGGESTION", + "summary": "required_evidence se carga solo para comprobar vacío; el matching no usa su contenido.", + "acceptance_criteria": "Comentar que el contrato es presencia no vacía, no matching por contenido, o usar required_evidence en el emparejamiento.", + "blocking": false, + "resolution": "advisory" + } + ], + "findings_digest": "sha256:2aed592b43913ae1ed1285f964bfdccf24cc4afea15d53b6c7529ca961e0cfaf", + "correction_budget": { + "limit_lines": 200, + "used_lines": 0, + "failed_attempts": 0, + "max_failed_attempts": 3 + }, + "correction_history": [], + "validation_history": [], + "follow_ups": [ + { + "owner": "reliability", + "summary": "raw(), e2e y assurance-graph tests siguen inyectando collector en el sobre del worker; index.test.js queda 10/25 y el camino harness no cubre claims débiles en el test focal.", + "blocking": false + }, + { + "owner": "risk", + "summary": "input.collector worker con claim human-decision|external-unverified se reescribe a model-reported sin UNTRUSTED_COLLECTOR; no escala a clase fuerte.", + "blocking": false + }, + { + "owner": "readability", + "summary": "F-f979f00ae92cda6f sigue advisory: required_evidence solo comprueba vacío; el matching no usa su contenido.", + "blocking": false + } + ], + "pending_operation": null, + "pending_correction": null, + "terminal_reason": "all-remediation-slices-passed", + "remediation_schema_version": 2, + "remediation_migration": { + "source_digest": "sha256:36d4b40e915332e25fa6eab957e6a8ffbbef35f4bdba4668679e3166ffafb614", + "manifest_digest": "sha256:82d7c22340c2520d29da7ae4789a1a102cb1aa7b0c7621f8765ea82220c047c5", + "legacy_used_lines": 0, + "legacy_failed_attempts": 0, + "migrated_at": "deterministic" + }, + "slice_order": [ + "S-ea4088e8a61de9f8", + "S-ad5558b5639b6890" + ], + "active_slice_id": null, + "correction_slices": { + "S-ea4088e8a61de9f8": { + "slice_id": "S-ea4088e8a61de9f8", + "root_cause_key": "collector-trust-boundary", + "finding_ids": [ + "F-d5739d79237afeb8" + ], + "evidence_digests": [ + "sha256:ac3226909e8e62a728ac5f7365f5f05dd0e5b843fe2c2ae6b28d8209ee567df0" + ], + "permitted_paths": [ + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "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/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.js", + "scripts/lib/independent-verifier/index.test.js" + ], + "resolutions": { + "F-d5739d79237afeb8": "unresolved" + }, + "status": "passed", + "used_lines": 200, + "failed_attempts": 1, + "limit_lines": 200, + "max_failed_attempts": 3, + "correction_history": [ + { + "slice_id": "S-ea4088e8a61de9f8", + "request_id": "slice-1-correction-start", + "finding_ids": [ + "F-d5739d79237afeb8" + ], + "paths": [ + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "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/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.js", + "scripts/lib/independent-verifier/index.test.js" + ], + "base_candidate_id": "sha256:89e60d916894d18b590e430d48c2be9afeb1c22fb1286ee3548d43be3e8c1a33", + "forecast_lines": 180, + "actual_changed_lines": 177, + "record_request_id": "slice-1-correction-record", + "corrected_candidate_id": "sha256:86c8e1cfbf9e8c85562d9ce361e6282692d1f56b9fd44b42b6289a2e5f83684c" + }, + { + "slice_id": "S-ea4088e8a61de9f8", + "request_id": "slice-1-correction-start-002", + "finding_ids": [ + "F-d5739d79237afeb8" + ], + "paths": [ + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "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/collector-provenance.js", + "scripts/lib/independent-verifier/evidence.js", + "scripts/lib/independent-verifier/index.js", + "scripts/lib/independent-verifier/index.test.js" + ], + "base_candidate_id": "sha256:86c8e1cfbf9e8c85562d9ce361e6282692d1f56b9fd44b42b6289a2e5f83684c", + "forecast_lines": 23, + "actual_changed_lines": 23, + "record_request_id": "slice-1-correction-record-002", + "corrected_candidate_id": "sha256:b0b5e5e2e753a8d01295b73f355f9b219241cd07e039317528b4a70a80e55cb1" + } + ], + "validation_history": [ + { + "request_id": "slice-1-validation-001", + "outcomes": [ + { + "id": "F-d5739d79237afeb8", + "status": "unresolved" + } + ], + "regression": { + "detected": false, + "evidence": [ + "static: resolveEvidenceProvenance still reads raw.collector from the same object as bytes/origin", + "apply-progress: 48 focal tests pass; no passed slices exist" + ], + "impacted_slices": [] + }, + "result": "failed" + }, + { + "request_id": "slice-1-validation-002", + "outcomes": [ + { + "id": "F-d5739d79237afeb8", + "status": "resolved" + } + ], + "regression": { + "detected": false, + "evidence": [ + "static: resolveEvidenceProvenance(raw, harnessCollector) returns UNTRUSTED_COLLECTOR if raw.collector exists; deriveProvenanceClass uses only harnessCollector; trusted mismatch (including weak vs allowlisted) fails closed", + "node --test --test-name-pattern \"envelope collector fails closed\" scripts/lib/independent-verifier/index.test.js → GREEN (apply-progress Batch 3)", + "index.test.js 10 pass / 25 fail because raw() still attaches collector to the envelope; no passed slices exist" + ], + "impacted_slices": [] + }, + "result": "passed" + } + ], + "regression_history": [], + "legacy_correction_refs": [], + "legacy_validation_refs": [] + }, + "S-ad5558b5639b6890": { + "slice_id": "S-ad5558b5639b6890", + "root_cause_key": "must-walk-coverage", + "finding_ids": [ + "F-00f97ff647d28eea", + "F-b3d6518c12aa69fe", + "F-ef73f7e16cab6436" + ], + "evidence_digests": [ + "sha256:270034ea7fc431fca6eb076013558fd7f1ba3bb0930ca17480a225200790f5d4", + "sha256:28be017893b5d745c4e280a88526d5bf84c6d3fe9c470fb141a7a8174d8c03d7", + "sha256:c1c2c8897304063d9aa3a19fc517c270e0851af622368010ae8d0a65758379c4" + ], + "permitted_paths": [ + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "scripts/lib/independent-verifier/obligation-coverage.js", + "scripts/lib/independent-verifier/obligation-coverage.test.js" + ], + "resolutions": { + "F-00f97ff647d28eea": "unresolved", + "F-b3d6518c12aa69fe": "unresolved", + "F-ef73f7e16cab6436": "unresolved" + }, + "status": "passed", + "used_lines": 121, + "failed_attempts": 0, + "limit_lines": 200, + "max_failed_attempts": 3, + "correction_history": [ + { + "slice_id": "S-ad5558b5639b6890", + "request_id": "slice-2-correction-start", + "finding_ids": [ + "F-00f97ff647d28eea", + "F-b3d6518c12aa69fe", + "F-ef73f7e16cab6436" + ], + "paths": [ + "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md", + "scripts/lib/independent-verifier/obligation-coverage.js", + "scripts/lib/independent-verifier/obligation-coverage.test.js" + ], + "base_candidate_id": "sha256:b0b5e5e2e753a8d01295b73f355f9b219241cd07e039317528b4a70a80e55cb1", + "forecast_lines": 130, + "actual_changed_lines": 121, + "record_request_id": "slice-2-correction-record", + "corrected_candidate_id": "sha256:e3b5973eff9d265352006b40009a7a349da73588f1e24777e15d1f12c3b71424" + } + ], + "validation_history": [ + { + "request_id": "slice-2-validation-001", + "outcomes": [ + { + "id": "F-00f97ff647d28eea", + "status": "resolved" + }, + { + "id": "F-b3d6518c12aa69fe", + "status": "resolved" + }, + { + "id": "F-ef73f7e16cab6436", + "status": "resolved" + } + ], + "regression": { + "detected": false, + "evidence": [ + "static: obligation-coverage.js only tightened missing/non-array graph to BINDING_MISMATCH and UNFULFILLED_MUST wording (evidence, not assessment); collector-trust-boundary paths were not in this delta", + "node --test scripts/lib/independent-verifier/obligation-coverage.test.js → 10 pass / 0 fail; no attributable break of S-ea4088e8a61de9f8 / F-d5739d79237afeb8" + ], + "impacted_slices": [] + }, + "result": "passed" + } + ], + "regression_history": [], + "legacy_correction_refs": [], + "legacy_validation_refs": [] + } + } +} \ No newline at end of file diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-lenses.js b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-lenses.js new file mode 100644 index 00000000..0634335a --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-lenses.js @@ -0,0 +1,42 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const { recordLensResult, freezeFindings } = require("../../../../scripts/lib/review-lineage.js"); + +const dir = __dirname; +let lineage = JSON.parse(fs.readFileSync(path.join(dir, "lineage.json"), "utf8")); + +const lenses = { + risk: require("./lens-risk-findings.json"), + reliability: require("./lens-reliability-findings.json"), + resilience: require("./lens-resilience-findings.json"), + readability: require("./lens-readability-findings.json"), +}; + +for (const dimension of ["risk", "reliability", "resilience", "readability"]) { + lineage = recordLensResult(lineage, { + dimension, + request_id: `lens-${dimension}-001`, + expected_revision: lineage.revision, + result: { findings: lenses[dimension] }, + }); +} + +lineage = freezeFindings(lineage, { + request_id: "freeze-findings-001", + expected_revision: lineage.revision, +}); + +fs.writeFileSync(path.join(dir, "lineage.json"), JSON.stringify(lineage, null, 2)); + +const counts = { BLOCKER: 0, CRITICAL: 0, WARNING: 0, SUGGESTION: 0 }; +for (const f of lineage.findings) counts[f.severity] += 1; +const summary = `${counts.BLOCKER} BLOCKER, ${counts.CRITICAL} CRITICAL, ${counts.WARNING} WARNING, ${counts.SUGGESTION} SUGGESTION`; +console.log(JSON.stringify({ + status: lineage.status, + terminal_reason: lineage.terminal_reason, + revision: lineage.revision, + findings_summary: summary, + findings: lineage.findings.map((f) => ({ id: f.id, owner: f.owner, severity: f.severity, blocking: f.blocking })), +}, null, 2)); diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-slice1-retry2.js b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-slice1-retry2.js new file mode 100644 index 00000000..3e38ee1a --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-slice1-retry2.js @@ -0,0 +1,48 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const { recordCorrection } = require("../../../../scripts/lib/review-lineage.js"); + +const dir = __dirname; +const ROOT = path.resolve(__dirname, "../../../.."); +const lineagePath = path.join(dir, "lineage.json"); +let lineage = JSON.parse(fs.readFileSync(lineagePath, "utf8")); +const pending = lineage.pending_correction; +const genesis = lineage.genesis.candidate; + +function sha256Bytes(buf) { + return `sha256:${crypto.createHash("sha256").update(buf).digest("hex")}`; +} + +const fileDigests = {}; +for (const rel of genesis.paths) { + fileDigests[rel] = sha256Bytes(fs.readFileSync(path.join(ROOT, rel))); +} + +lineage = recordCorrection(lineage, { + request_id: "slice-1-correction-record-002", + expected_revision: lineage.revision, + base_candidate_id: pending.base_candidate_id, + paths: pending.paths, + actual_changed_lines: 23, + corrected_candidate: { + projection: genesis.projection, + base_tree: genesis.base_tree, + candidate_tree: sha256Bytes(Buffer.from(JSON.stringify(fileDigests))), + paths: genesis.paths.slice(), + diff_hash: genesis.diff_hash, + paths_digest: genesis.paths_digest, + authored_lines: genesis.authored_lines, + original_changed_lines: genesis.original_changed_lines, + }, +}); + +fs.writeFileSync(lineagePath, JSON.stringify(lineage, null, 2)); +console.log(JSON.stringify({ + status: lineage.status, + revision: lineage.revision, + used_lines: lineage.correction_slices[lineage.active_slice_id].used_lines, + current_candidate_id: lineage.current_candidate_id, +}, null, 2)); diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-slice1.js b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-slice1.js new file mode 100644 index 00000000..c8f5bf43 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-slice1.js @@ -0,0 +1,51 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const { recordCorrection } = require("../../../../scripts/lib/review-lineage.js"); + +const dir = __dirname; +const ROOT = path.resolve(__dirname, "../../../.."); +const lineagePath = path.join(dir, "lineage.json"); +let lineage = JSON.parse(fs.readFileSync(lineagePath, "utf8")); + +function sha256Bytes(buf) { + return `sha256:${crypto.createHash("sha256").update(buf).digest("hex")}`; +} + +const pending = lineage.pending_correction; +const genesis = lineage.genesis.candidate; +const fileDigests = {}; +for (const rel of genesis.paths) { + fileDigests[rel] = sha256Bytes(fs.readFileSync(path.join(ROOT, rel))); +} + +const corrected = { + projection: genesis.projection, + base_tree: genesis.base_tree, + candidate_tree: sha256Bytes(Buffer.from(JSON.stringify(fileDigests))), + paths: genesis.paths.slice(), + diff_hash: sha256Bytes(fs.readFileSync(path.join(dir, "unified.diff"))), + paths_digest: genesis.paths_digest, + authored_lines: genesis.authored_lines, + original_changed_lines: genesis.original_changed_lines, +}; + +lineage = recordCorrection(lineage, { + request_id: "slice-1-correction-record", + expected_revision: lineage.revision, + base_candidate_id: pending.base_candidate_id, + paths: pending.paths, + actual_changed_lines: 177, + corrected_candidate: corrected, +}); + +fs.writeFileSync(lineagePath, JSON.stringify(lineage, null, 2)); +console.log(JSON.stringify({ + status: lineage.status, + revision: lineage.revision, + current_candidate_id: lineage.current_candidate_id, + active_slice_id: lineage.active_slice_id, + used_lines: lineage.correction_slices[lineage.active_slice_id].used_lines, +}, null, 2)); diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-slice2.js b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-slice2.js new file mode 100644 index 00000000..1bd64c35 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/record-slice2.js @@ -0,0 +1,48 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const { recordCorrection } = require("../../../../scripts/lib/review-lineage.js"); + +const dir = __dirname; +const ROOT = path.resolve(__dirname, "../../../.."); +const lineagePath = path.join(dir, "lineage.json"); +let lineage = JSON.parse(fs.readFileSync(lineagePath, "utf8")); +const pending = lineage.pending_correction; +const genesis = lineage.genesis.candidate; + +function sha256Bytes(buf) { + return `sha256:${crypto.createHash("sha256").update(buf).digest("hex")}`; +} + +const fileDigests = {}; +for (const rel of genesis.paths) { + fileDigests[rel] = sha256Bytes(fs.readFileSync(path.join(ROOT, rel))); +} + +lineage = recordCorrection(lineage, { + request_id: "slice-2-correction-record", + expected_revision: lineage.revision, + base_candidate_id: pending.base_candidate_id, + paths: pending.paths, + actual_changed_lines: 121, + corrected_candidate: { + projection: genesis.projection, + base_tree: genesis.base_tree, + candidate_tree: sha256Bytes(Buffer.from(JSON.stringify(fileDigests))), + paths: genesis.paths.slice(), + diff_hash: genesis.diff_hash, + paths_digest: genesis.paths_digest, + authored_lines: genesis.authored_lines, + original_changed_lines: genesis.original_changed_lines, + }, +}); + +fs.writeFileSync(lineagePath, JSON.stringify(lineage, null, 2)); +console.log(JSON.stringify({ + status: lineage.status, + revision: lineage.revision, + used_lines: lineage.correction_slices[lineage.active_slice_id].used_lines, + current_candidate_id: lineage.current_candidate_id, +}, null, 2)); diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/unified.diff b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/unified.diff new file mode 100644 index 00000000..92574dbc --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/.4r/unified.diff @@ -0,0 +1,3599 @@ +diff --git a/docs/architecture/harness-evolution.md b/docs/architecture/harness-evolution.md +index a81e228..27fe330 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.50.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** están entregados y archivados. OpenSpec/Git/Candidate siguen siendo la única autoridad semántica; el Assurance Graph es proyección. **K6c** es la siguiente iniciativa elegible. ++> **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; ahora `revise` por `k6b-verification-integrity-remediation`). OpenSpec/Git/Candidate siguen siendo la única autoridad semántica; el Assurance Graph es proyección. **K6c** está `blocked-by-K6b-remediation` hasta archive de esa remediación. + > **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` | + | 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)~~ — verifier, strategies/provenance y proyección implementados; autoridad del grafo, K6c ChallengePlan y K6d complexity delta siguen pendientes. ++11. ~~K6b: verifier + provenance + Assurance Graph (proyección)~~ — verifier, strategies/provenance y proyección implementados; **estado `revise`** (`k6b-verification-integrity-remediation`) hasta archive; K6c ChallengePlan permanece `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 8233c05..fd4328f 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 como proyección derivada (no autoridad); archivado y publicado en v2.50.0 (`2026-08-27-k6b-verifier-evidence-assurance-graph`) | +-| `next-eligible` | **K6c** | ChallengePlan policy-selected | ++| `revise` | **K6b** | Verifier independiente, evidence strategies/provenance y Assurance Graph; remediación de integridad en curso (`k6b-verification-integrity-remediation`) | ++| `blocked-by-K6b-remediation` | **K6c** | ChallengePlan policy-selected | + | `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-eligible: K6c ++Blocked: 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`. Change OpenSpec `k6b-verifier-evidence-assurance-graph` archivado y publicado en v2.50.0: verifier independiente, strategies/provenance y Assurance Graph como proyección derivada. OpenSpec/Git/Candidate siguen siendo la única autoridad semántica. El grafo no concede lifecycle, approval ni delivery. K6c es next-eligible. ++**Estado:** `revise`. Change OpenSpec `k6b-verification-integrity-remediation` en apply: cobertura MUST del Obligation Manifest, assessments persistibles, provenance de collector, `graph_id` canónico y proyección fail-closed. El archive de `k6b-verifier-evidence-assurance-graph` (v2.50.0) permanece; 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 permanece `blocked-by-K6b-remediation` hasta archive de esta remediación.** + + **Absorbe/rebasa:** P12/P16; O15; separación apply/verify vigente. + +@@ -1103,9 +1103,9 @@ 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`. K6c no empieza antes. ++**Gate terminal:** verifier, strategy selector, provenance y Assurance Graph (proyección) conformes; checkpoint evidencia/challenges → `continue` | `revise` | `reject`. K6c no empieza antes de que esta remediación archive. + +-### K6c — adversarial challenges (policy-selected) — **pending** ++### K6c — adversarial challenges (policy-selected) — **blocked-by-K6b-remediation** + + **Dependencias:** K6b (+ `PolicySnapshot` / strategy de evidencia). + +@@ -2060,3 +2060,4 @@ Un Change Program (objetivo → children OpenSpec + cursor, ver investigación ` + - 2026-08-27: reconciliación aditiva (sin mover ruta crítica ni next-eligible): se nombra la distinción proceso intra-change vs Change Program inter-change; first-match de la tabla viva es compatibilidad, no K10; K6b/R4/K10 no absorben ese hueco. Investigación no normativa `docs/architecture/research/proportional-process-and-change-program.md`. Arquitectura: corte conceptual de la misma fecha; deuda stale K3/K4a/K5/K6a/K4b reconciliada. + - 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. +diff --git a/schemas/kernel/assurance-graph/v1.schema.json b/schemas/kernel/assurance-graph/v1.schema.json +index bd3f4c2..0556996 100644 +--- a/schemas/kernel/assurance-graph/v1.schema.json ++++ b/schemas/kernel/assurance-graph/v1.schema.json +@@ -43,6 +43,9 @@ + }, + "equivalence_manifest": { + "$ref": "#/$defs/equivalenceManifest" ++ }, ++ "canonical_inputs": { ++ "$ref": "#/$defs/canonicalInputs" + } + }, + "additionalProperties": false, +@@ -123,6 +126,28 @@ + } + }, + "additionalProperties": false ++ }, ++ "canonicalInputs": { ++ "type": "object", ++ "additionalProperties": false, ++ "properties": { ++ "contract_digest": { ++ "type": "string", ++ "pattern": "^sha256:[a-f0-9]{64}$" ++ }, ++ "policy_snapshot_id": { ++ "type": "string", ++ "pattern": "^sha256:[a-f0-9]{64}$" ++ }, ++ "execution_graph_digest": { ++ "type": "string", ++ "pattern": "^sha256:[a-f0-9]{64}$" ++ }, ++ "openspec_input_digest": { ++ "type": "string", ++ "pattern": "^sha256:[a-f0-9]{64}$" ++ } ++ } + } + } + } +diff --git a/schemas/kernel/contract-claims.json b/schemas/kernel/contract-claims.json +index a80c4cb..31d2ecc 100644 +--- a/schemas/kernel/contract-claims.json ++++ b/schemas/kernel/contract-claims.json +@@ -678,6 +678,21 @@ + ] + }, + "command_shapes": [] ++ }, ++ "assessment": { ++ "required_fields": [ ++ "schema_version", ++ "kind", ++ "assessment_id", ++ "evidence_id", ++ "role", ++ "obligation_id", ++ "node_id", ++ "candidate_id", ++ "policy_snapshot_id" ++ ], ++ "enum_values": {}, ++ "command_shapes": [] + } + } + } +diff --git a/schemas/kernel/manifest.json b/schemas/kernel/manifest.json +index 52b508c..3936794 100644 +--- a/schemas/kernel/manifest.json ++++ b/schemas/kernel/manifest.json +@@ -220,6 +220,11 @@ + "path": "schemas/kernel/assurance-graph/v1.schema.json", + "$id": "ospec://schemas/kernel/assurance-graph/v1", + "schema_version": 1 ++ }, ++ "assessment": { ++ "path": "schemas/kernel/assessment/v1.schema.json", ++ "$id": "ospec://schemas/kernel/assessment/v1", ++ "schema_version": 1 + } + } + } +diff --git a/scripts/k6b-verifier-assurance-graph-e2e.test.js b/scripts/k6b-verifier-assurance-graph-e2e.test.js +index fa21324..5af0192 100644 +--- a/scripts/k6b-verifier-assurance-graph-e2e.test.js ++++ b/scripts/k6b-verifier-assurance-graph-e2e.test.js +@@ -12,6 +12,8 @@ const { verifyCandidate } = require("./lib/independent-verifier/index.js"); + const { + projectAssuranceGraph, + computeInvalidationClosure, ++ replayAssuranceGraph, ++ reconcileAssuranceGraph, + } = require("./lib/assurance-graph/index.js"); + + const CONFIG_PATH = path.resolve(__dirname, "..", "openspec", "config.yaml"); +@@ -49,6 +51,8 @@ function featureEvidence() { + ]; + } + ++const HARNESS_COLLECTOR = { id: "node-test", transport: "tool-execution-transport" }; ++ + test("E2E K6b: K4b-frozen Candidate → verify → project twice → successor invalidation rejects stale evidence", () => { + const configBefore = fs.readFileSync(CONFIG_PATH, "utf8"); + const files = { "src/index.js": "function add(a, b) { return a + b; }\nmodule.exports = { add };\n" }; +@@ -87,30 +91,63 @@ test("E2E K6b: K4b-frozen Candidate → verify → project twice → successor i + contract, + repository: { files }, + declaredStrategy: "feature", ++ collector: HARNESS_COLLECTOR, + rawEvidence: featureEvidence(), + }); + assert.equal(verified.ok, true, verified.error || verified.reason_code); + assert.equal(verified.verification.verdict, "PASS"); + assert.equal(verified.evidence.some((ev) => Object.prototype.hasOwnProperty.call(ev, "verdict")), false); + +- const classified = verified.evidence.map((evidence) => ({ evidence, obligation_ids: ["req-repair-001"] })); ++ const classified = verified.evidence.map((evidence) => ({ evidence })); + const firstGraph = projectAssuranceGraph({ + candidate: predecessor, + executionGraph, + evidence: classified, ++ assessments: verified.assessments, + verification: verified.verification, ++ canonicalInputs: verified.assurance_graph.canonical_inputs, + }); + const secondGraph = projectAssuranceGraph({ + candidate: predecessor, + executionGraph, + evidence: [...classified].reverse(), ++ assessments: [...verified.assessments].reverse(), + verification: verified.verification, ++ canonicalInputs: verified.assurance_graph.canonical_inputs, + }); + assert.equal(firstGraph.ok, true); + assert.equal(secondGraph.ok, true); + assert.equal(firstGraph.graph.graph_id, secondGraph.graph.graph_id); + assert.deepEqual(firstGraph.graph.edges, secondGraph.graph.edges); + assert.equal(verified.assurance_graph.graph_id, firstGraph.graph.graph_id); ++ assert.ok(Array.isArray(verified.assessments) && verified.assessments.length >= 1); ++ assert.ok(verified.assurance_graph.canonical_inputs); ++ ++ const replayed = replayAssuranceGraph({ ++ candidate: predecessor, ++ executionGraph, ++ evidence: verified.evidence, ++ assessments: verified.assessments, ++ verification: verified.verification, ++ canonical_inputs: verified.assurance_graph.canonical_inputs, ++ }); ++ assert.equal(replayed.ok, true); ++ assert.equal(replayed.graph.graph_id, verified.assurance_graph.graph_id); ++ assert.deepEqual(replayed.graph.edges, verified.assurance_graph.edges); ++ ++ const churned = reconcileAssuranceGraph(verified.assurance_graph, { ++ candidate: predecessor, ++ executionGraph, ++ evidence: verified.evidence, ++ assessments: verified.assessments, ++ verification: verified.verification, ++ canonicalInputs: { ++ ...verified.assurance_graph.canonical_inputs, ++ contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", ++ }, ++ }); ++ assert.equal(churned.ok, false); ++ assert.equal(churned.reason_code, "GRAPH_DIVERGENCE"); + + const successorFiles = { "src/index.js": "function add(a, b) { return a + b + 1; }\nmodule.exports = { add };\n" }; + const successorTree = computeTreeDigest(successorFiles); +@@ -156,6 +193,7 @@ test("E2E K6b: K4b-frozen Candidate → verify → project twice → successor i + contract, + repository: { files }, + declaredStrategy: "feature", ++ collector: HARNESS_COLLECTOR, + rawEvidence: featureEvidence(), + priorAssuranceGraph: graphForClosure, + }); +diff --git a/scripts/lib/assurance-graph/index.js b/scripts/lib/assurance-graph/index.js +index 86f67cd..400d375 100644 +--- a/scripts/lib/assurance-graph/index.js ++++ b/scripts/lib/assurance-graph/index.js +@@ -1,6 +1,6 @@ + "use strict"; + +-const { projectAssuranceGraph } = require("./projector.js"); ++const { projectAssuranceGraph, rejectForbidden } = require("./projector.js"); + const { computeInvalidationClosure, isEvidenceTransitivelyInvalidated } = require("./invalidation.js"); + + function fail(reason_code, error) { +@@ -8,7 +8,7 @@ function fail(reason_code, error) { + } + + /** +- * Recompute the projection from canonical inputs and fail closed on divergence. ++ * Recompute the projection from persistable outputs and fail closed on divergence. + * + * @param {object} stored + * @param {object} canonicalInput +@@ -30,6 +30,24 @@ function reconcileAssuranceGraph(stored, canonicalInput) { + return { ok: true, graph: projected.graph }; + } + ++/** ++ * Replay a projection from persistable assessments, evidence, verification, and canonical_inputs. ++ * Never consumes ephemeral projector obligation_ids. ++ * ++ * @param {object} persistable ++ * @returns {{ ok: true, graph: object } | { ok: false, reason_code: string }} ++ */ ++function replayAssuranceGraph(persistable = {}) { ++ return projectAssuranceGraph({ ++ canonicalInputs: persistable.canonical_inputs || persistable.canonicalInputs, ++ candidate: persistable.candidate, ++ executionGraph: persistable.executionGraph, ++ evidence: persistable.evidence, ++ assessments: persistable.assessments, ++ verification: persistable.verification, ++ }); ++} ++ + function emitEquivalenceManifest(graph) { + if (!graph || typeof graph.graph_id !== "string" || typeof graph.candidate_id !== "string") { + return fail("GRAPH_DIVERGENCE", "manifest requires graph_id and candidate_id"); +@@ -57,6 +75,8 @@ function rejectAuthorityMisuse(_intent) { + module.exports = { + projectAssuranceGraph, + reconcileAssuranceGraph, ++ replayAssuranceGraph, ++ rejectForbidden, + computeInvalidationClosure, + isEvidenceTransitivelyInvalidated, + emitEquivalenceManifest, +diff --git a/scripts/lib/assurance-graph/index.test.js b/scripts/lib/assurance-graph/index.test.js +index 7a4c35b..b31bc70 100644 +--- a/scripts/lib/assurance-graph/index.test.js ++++ b/scripts/lib/assurance-graph/index.test.js +@@ -9,6 +9,8 @@ const { computeTreeDigest } = require("../worker-workspace.js"); + const { + projectAssuranceGraph, + reconcileAssuranceGraph, ++ replayAssuranceGraph, ++ rejectForbidden, + computeInvalidationClosure, + emitEquivalenceManifest, + rejectAuthorityMisuse, +@@ -81,6 +83,8 @@ function featureRaw() { + ]; + } + ++const HARNESS_COLLECTOR = { id: "node-test", transport: "tool-execution-transport" }; ++ + function verifiedProjection() { + const files = { "src/index.js": "module.exports = 1;\n" }; + const candidate = freezeFromFiles(files); +@@ -90,6 +94,7 @@ function verifiedProjection() { + executionGraph, + repository: { files }, + declaredStrategy: "feature", ++ collector: HARNESS_COLLECTOR, + rawEvidence: featureRaw(), + }); + assert.equal(verified.ok, true, verified.error || verified.reason_code); +@@ -100,19 +105,20 @@ test("REQ-assurance-graph-002: same inputs yield the same digest and edges despi + const { candidate, executionGraph, verified } = verifiedProjection(); + const classified = verified.evidence.map((evidence, index) => ({ + evidence, +- obligation_ids: ["req-repair-001"], + role: ["acceptance", "invariants", "contract", "negative"][index], + })); + const first = projectAssuranceGraph({ + candidate, + executionGraph, + evidence: classified, ++ assessments: verified.assessments, + verification: verified.verification, + }); + const second = projectAssuranceGraph({ + candidate, + executionGraph, + evidence: [...classified].reverse(), ++ assessments: [...verified.assessments].reverse(), + verification: verified.verification, + additionalEdges: [...(first.graph.edges || [])].reverse(), + }); +@@ -148,14 +154,16 @@ test("REQ-assurance-graph-001: matching canonical inputs project; divergence fai + const projected = projectAssuranceGraph({ + candidate, + executionGraph, +- evidence: verified.evidence.map((evidence) => ({ evidence, obligation_ids: ["req-repair-001"] })), ++ evidence: verified.evidence, ++ assessments: verified.assessments, + verification: verified.verification, + }); + assert.equal(projected.ok, true); + const reconciled = reconcileAssuranceGraph(projected.graph, { + candidate, + executionGraph, +- evidence: verified.evidence.map((evidence) => ({ evidence, obligation_ids: ["req-repair-001"] })), ++ evidence: verified.evidence, ++ assessments: verified.assessments, + verification: verified.verification, + }); + assert.equal(reconciled.ok, true); +@@ -164,7 +172,8 @@ test("REQ-assurance-graph-001: matching canonical inputs project; divergence fai + const diverged = reconcileAssuranceGraph(mutated, { + candidate, + executionGraph, +- evidence: verified.evidence.map((evidence) => ({ evidence, obligation_ids: ["req-repair-001"] })), ++ evidence: verified.evidence, ++ assessments: verified.assessments, + verification: verified.verification, + }); + assert.equal(diverged.ok, false); +@@ -178,7 +187,8 @@ test("REQ-assurance-graph-001: matching canonical inputs project; divergence fai + { + candidate, + executionGraph, +- evidence: verified.evidence.map((evidence) => ({ evidence, obligation_ids: ["req-repair-001"] })), ++ evidence: verified.evidence, ++ assessments: verified.assessments, + verification: verified.verification, + } + ); +@@ -193,7 +203,8 @@ test("REQ-harness-authority-canon-010: APIs return new objects without write-thr + const projected = projectAssuranceGraph({ + candidate, + executionGraph, +- evidence: verified.evidence.map((evidence) => ({ evidence, obligation_ids: ["req-repair-001"] })), ++ evidence: verified.evidence, ++ assessments: verified.assessments, + verification: verified.verification, + additionalNodes: nodes, + }); +@@ -291,3 +302,129 @@ test("REQ-harness-authority-canon-010: graph used as approval or delivery author + assert.equal(result.ok, false); + assert.equal(result.reason_code, "GRAPH_AUTHORITY_MISUSE"); + }); ++ ++test("REQ-assurance-graph-002: graph_id changes when canonical inputs change; permutation does not", () => { ++ const { candidate, executionGraph, verified } = verifiedProjection(); ++ const baseInput = { ++ candidate, ++ executionGraph, ++ evidence: verified.evidence, ++ assessments: verified.assessments, ++ verification: verified.verification, ++ }; ++ const base = projectAssuranceGraph(baseInput); ++ assert.equal(base.ok, true); ++ assert.ok(base.graph.canonical_inputs); ++ ++ const flippedContract = projectAssuranceGraph({ ++ ...baseInput, ++ canonicalInputs: { ++ ...base.graph.canonical_inputs, ++ contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", ++ }, ++ }); ++ assert.equal(flippedContract.ok, true); ++ assert.notEqual(base.graph.graph_id, flippedContract.graph.graph_id); ++ ++ const flippedPolicy = projectAssuranceGraph({ ++ ...baseInput, ++ canonicalInputs: { ++ ...base.graph.canonical_inputs, ++ policy_snapshot_id: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", ++ }, ++ }); ++ assert.notEqual(base.graph.graph_id, flippedPolicy.graph.graph_id); ++ ++ const flippedExec = projectAssuranceGraph({ ++ ...baseInput, ++ canonicalInputs: { ++ ...base.graph.canonical_inputs, ++ execution_graph_digest: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", ++ }, ++ }); ++ assert.notEqual(base.graph.graph_id, flippedExec.graph.graph_id); ++ ++ const flippedOpenspec = projectAssuranceGraph({ ++ ...baseInput, ++ canonicalInputs: { ++ ...base.graph.canonical_inputs, ++ openspec_input_digest: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", ++ }, ++ }); ++ assert.notEqual(base.graph.graph_id, flippedOpenspec.graph.graph_id); ++ ++ const permutedNodes = projectAssuranceGraph({ ++ ...baseInput, ++ additionalNodes: [...base.graph.nodes].reverse(), ++ }); ++ assert.equal(permutedNodes.ok, true); ++ assert.equal(base.graph.graph_id, permutedNodes.graph.graph_id); ++}); ++ ++test("REQ-assurance-graph-005: rejectForbidden matches kind/namespace, not id substring", () => { ++ const allowed = rejectForbidden( ++ [{ id: "REQ-add-authorization-header", kind: "requirement" }], ++ [] ++ ); ++ assert.equal(allowed.ok, true); ++ ++ const structured = rejectForbidden([{ id: "authz-1", kind: "authorization" }], []); ++ assert.equal(structured.ok, false); ++ assert.equal(structured.reason_code, "FORBIDDEN_RELATION"); ++ ++ const namespaced = rejectForbidden( ++ [{ id: "harmless-id", kind: "requirement", namespace: "attestation" }], ++ [] ++ ); ++ assert.equal(namespaced.ok, false); ++ assert.equal(namespaced.reason_code, "FORBIDDEN_RELATION"); ++}); ++ ++test("REQ-assurance-graph-001: missing candidate is GRAPH_PROJECTION_FAILED", () => { ++ const result = projectAssuranceGraph({}); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "GRAPH_PROJECTION_FAILED"); ++}); ++ ++test("REQ-assurance-graph-006: replay from persistable outputs is byte-identical; contract churn diverges", () => { ++ 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 replayed = replayAssuranceGraph(persistable); ++ assert.equal(replayed.ok, true); ++ assert.equal(replayed.graph.graph_id, verified.assurance_graph.graph_id); ++ assert.deepEqual(replayed.graph.edges, verified.assurance_graph.edges); ++ ++ const churned = projectAssuranceGraph({ ++ candidate, ++ executionGraph, ++ evidence: verified.evidence, ++ assessments: verified.assessments, ++ verification: verified.verification, ++ canonicalInputs: { ++ ...verified.assurance_graph.canonical_inputs, ++ contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", ++ }, ++ }); ++ assert.notEqual(churned.graph.graph_id, verified.assurance_graph.graph_id); ++ const diverged = reconcileAssuranceGraph(verified.assurance_graph, { ++ candidate, ++ executionGraph, ++ evidence: verified.evidence, ++ assessments: verified.assessments, ++ verification: verified.verification, ++ canonicalInputs: { ++ ...verified.assurance_graph.canonical_inputs, ++ contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", ++ }, ++ }); ++ assert.equal(diverged.ok, false); ++ assert.equal(diverged.reason_code, "GRAPH_DIVERGENCE"); ++}); ++ +diff --git a/scripts/lib/assurance-graph/projector.js b/scripts/lib/assurance-graph/projector.js +index dfc5f20..40ed240 100644 +--- a/scripts/lib/assurance-graph/projector.js ++++ b/scripts/lib/assurance-graph/projector.js +@@ -12,13 +12,19 @@ const ALLOWED_NODE_KINDS = Object.freeze([ + "test-evidence", + "verification-decision", + ]); +-const FORBIDDEN_NODE_KIND_MARKERS = Object.freeze([ ++const FORBIDDEN_KINDS = Object.freeze([ + "finding", + "attestation", + "authorization", + "evaluation-attestation", +- "reviewed-by", + ]); ++const FORBIDDEN_NAMESPACES = Object.freeze([ ++ "finding", ++ "attestation", ++ "authorization", ++ "evaluation-attestation", ++]); ++const SHA256 = /^sha256:[a-f0-9]{64}$/; + + function fail(reason_code, error) { + return { ok: false, reason_code, error: error || reason_code }; +@@ -56,17 +62,26 @@ function canonicalize(nodes, edges) { + return { nodes: canonicalNodes, edges: canonicalEdges }; + } + ++/** ++ * Accept or reject subjects by structured kind/namespace. Never scan id substrings. ++ * ++ * @param {object[]} nodes ++ * @param {object[]} edges ++ * @returns {{ ok: true } | { ok: false, reason_code: string, error?: string }} ++ */ + function rejectForbidden(nodes, edges) { ++ // One reason_code for kind, namespace, and relation; distinguish via error text. ++ // FORBIDDEN_KINDS is defense-in-depth: checked before the allowlist so it is reachable. + for (const node of nodes || []) { ++ if (FORBIDDEN_KINDS.includes(node.kind)) { ++ return fail("FORBIDDEN_RELATION", `forbidden kind ${node.kind}`); ++ } + if (!ALLOWED_NODE_KINDS.includes(node.kind)) { + return fail("FORBIDDEN_RELATION", `forbidden node kind ${node.kind}`); + } +- // Substring match on id+kind haystack: markers apply with includes() over +- // id or kind, not exact kind equality. ALLOWED_NODE_KINDS already covers +- // the exact kind allow-list above. +- const idAndKindHaystack = `${node.id} ${node.kind}`.toLowerCase(); +- if (FORBIDDEN_NODE_KIND_MARKERS.some((marker) => idAndKindHaystack.includes(marker))) { +- return fail("FORBIDDEN_RELATION", `forbidden subject ${node.id}`); ++ const namespace = typeof node.namespace === "string" ? node.namespace.toLowerCase() : ""; ++ if (namespace && FORBIDDEN_NAMESPACES.includes(namespace)) { ++ return fail("FORBIDDEN_RELATION", `forbidden namespace ${node.namespace}`); + } + } + for (const edge of edges || []) { +@@ -87,6 +102,39 @@ function pushEdge(edges, from, relation, to) { + edges.push({ from, relation, to }); + } + ++function resolveCanonicalInputDigests(input) { ++ const provided = input.canonicalInputs && typeof input.canonicalInputs === "object" ? input.canonicalInputs : {}; ++ const graph = input.executionGraph || {}; ++ const contract = provided.contract && typeof provided.contract === "object" ? provided.contract : {}; ++ ++ const contractDigest = provided.contract_digest || contract.contract_digest || graph.contract_digest || null; ++ const policySnapshotId = provided.policy_snapshot_id || graph.policy_snapshot_id || null; ++ const executionGraphDigest = provided.execution_graph_digest || graph.graph_id || null; ++ const openspecInputDigest = ++ provided.openspec_input_digest || ++ sha256Fingerprint("openspec-input/v1", { ++ contract_digest: contractDigest, ++ source_snapshot_id: graph.source_snapshot_id || (provided.sourceSnapshot && provided.sourceSnapshot.source_snapshot_id) || null, ++ }); ++ ++ return { ++ 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]; ++ } ++ } ++ return persistable; ++} ++ + /** + * Derive a canonical Assurance Graph projection. Returns a new object. + * +@@ -96,7 +144,7 @@ function pushEdge(edges, from, relation, to) { + function projectAssuranceGraph(input = {}) { + const candidate = input.candidate; + if (!candidate || typeof candidate.candidate_id !== "string") { +- return fail("GRAPH_DIVERGENCE", "frozen candidate is required to project"); ++ return fail("GRAPH_PROJECTION_FAILED", "frozen candidate is required to project"); + } + + const nodes = []; +@@ -128,11 +176,16 @@ function projectAssuranceGraph(input = {}) { + if (!record || !record.evidence_id) continue; + pushNode(nodes, record.evidence_id, "test-evidence"); + pushEdge(edges, record.evidence_id, "derived-from", candidateId); +- const obligationIds = item.obligation_ids || record.obligation_ids || []; +- for (const obligationId of obligationIds) { +- pushNode(nodes, obligationId, "requirement"); +- pushEdge(edges, record.evidence_id, "satisfies", obligationId); +- } ++ } ++ ++ // Persistable assessments become evidence→obligation `satisfies` edges. ++ // Assessment is not a node; distinct roles of the same pair collapse via canonicalize. ++ const assessments = Array.isArray(input.assessments) ? input.assessments : []; ++ for (const assessment of assessments) { ++ if (!assessment || !assessment.evidence_id || !assessment.obligation_id) continue; ++ pushNode(nodes, assessment.evidence_id, "test-evidence"); ++ pushNode(nodes, assessment.obligation_id, "requirement"); ++ pushEdge(edges, assessment.evidence_id, "satisfies", assessment.obligation_id); + } + + const verification = input.verification; +@@ -155,27 +208,36 @@ function projectAssuranceGraph(input = {}) { + if (!forbidden.ok) return forbidden; + + const canonical = canonicalize(nodes, edges); ++ const canonicalInputs = resolveCanonicalInputDigests(input); + const graphId = sha256Fingerprint("assurance-graph/v1", { + 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, + nodes: canonical.nodes, + edges: canonical.edges, + }); + +- return { +- ok: true, +- graph: { +- schema_version: 1, +- kind: "assurance-graph/v1", +- graph_id: graphId, +- candidate_id: candidateId, +- nodes: canonical.nodes.map(cloneNode), +- edges: canonical.edges.map(cloneEdge), +- }, ++ const resultGraph = { ++ schema_version: 1, ++ kind: "assurance-graph/v1", ++ graph_id: graphId, ++ candidate_id: candidateId, ++ nodes: canonical.nodes.map(cloneNode), ++ edges: canonical.edges.map(cloneEdge), + }; ++ const persistedInputs = persistableCanonicalInputs(canonicalInputs); ++ if (Object.keys(persistedInputs).length > 0) { ++ resultGraph.canonical_inputs = persistedInputs; ++ } ++ ++ return { ok: true, graph: resultGraph }; + } + + module.exports = { + ALLOWED_RELATIONS, + canonicalize, ++ rejectForbidden, + projectAssuranceGraph, + }; +diff --git a/scripts/lib/contract-checkers/k1-schema-compat.js b/scripts/lib/contract-checkers/k1-schema-compat.js +index 4bab9f6..4583064 100644 +--- a/scripts/lib/contract-checkers/k1-schema-compat.js ++++ b/scripts/lib/contract-checkers/k1-schema-compat.js +@@ -67,6 +67,10 @@ const FAMILY_PUBLICATION = Object.freeze({ + verification: Object.freeze({ + fixtureNameFilter: (name) => !name.startsWith("v2-"), + }), ++ assessment: Object.freeze({ ++ // Bundle of four role payloads for identity distinctness; walked by k6b-schema-fixtures. ++ fixtureNameFilter: (name) => name !== "v1-four-roles.json", ++ }), + }); + + function toPosix(relativePath) { +diff --git a/scripts/lib/independent-verifier/evidence.js b/scripts/lib/independent-verifier/evidence.js +index 125e1bd..2a8a461 100644 +--- a/scripts/lib/independent-verifier/evidence.js ++++ b/scripts/lib/independent-verifier/evidence.js +@@ -4,6 +4,7 @@ const crypto = require("node:crypto"); + const path = require("node:path"); + const { sha256Fingerprint } = require("../canonical-json.js"); + const { validateInstance, loadSchemaById } = require("../kernel-schema-validator.js"); ++const { resolveEvidenceProvenance } = require("./collector-provenance.js"); + + const EVIDENCE_V2_ID = "ospec://schemas/kernel/evidence/v2"; + const DEFAULT_SCHEMA_ROOT = path.resolve(__dirname, "../../.."); +@@ -55,7 +56,7 @@ function computeEvidenceId(fields, rawBytes) { + * @param {object} [executionGraph] + * @returns {{ ok: true, evidence: object, role?: string, obligation_ids: string[] } | { ok: false, reason_code: string }} + */ +-function normalizeEvidence(raw, candidate, executionGraph) { ++function normalizeEvidence(raw, candidate, executionGraph, harnessCollector) { + if (!raw || typeof raw !== "object") { + return fail("FABRICATED_EVIDENCE", "raw evidence must be an object"); + } +@@ -85,11 +86,14 @@ function normalizeEvidence(raw, candidate, executionGraph) { + } + } + ++ const resolvedProvenance = resolveEvidenceProvenance(raw, harnessCollector); ++ if (!resolvedProvenance.ok) return resolvedProvenance; ++ + const record = { + schema_version: 2, + kind: "evidence/v2", + candidate_id: candidateId, +- provenance: raw.provenance, ++ provenance: resolvedProvenance.provenance, + origin: raw.origin, + digest, + node_id: nodeId, +diff --git a/scripts/lib/independent-verifier/index.js b/scripts/lib/independent-verifier/index.js +index b7f607b..fb0c30b 100644 +--- a/scripts/lib/independent-verifier/index.js ++++ b/scripts/lib/independent-verifier/index.js +@@ -2,45 +2,115 @@ + + const { validateBindings } = require("./bindings.js"); + const { selectStrategy, evaluateStrategy } = require("./strategy-policy.js"); +-const { normalizeEvidence } = require("./evidence.js"); ++const { normalizeEvidence, computeEvidenceId } = require("./evidence.js"); ++const { resolveEvidenceProvenance } = require("./collector-provenance.js"); + const { emitVerification } = require("./verdict.js"); +-const { +- projectAssuranceGraph, +- emitEquivalenceManifest, +- isEvidenceTransitivelyInvalidated, +-} = require("../assurance-graph/index.js"); ++const { walkMustObligations } = require("./obligation-coverage.js"); ++const assuranceGraph = require("../assurance-graph/index.js"); + + function fail(reason_code, error) { + return { ok: false, reason_code, error: error || reason_code }; + } + ++function channelCollector(input, index) { ++ if (Array.isArray(input.collectors)) return input.collectors[index]; ++ return input.collector; ++} ++ ++function mapProjectionFailure(projected) { ++ if (projected.reason_code === "GRAPH_DIVERGENCE") { ++ return fail("GRAPH_DIVERGENCE", projected.error); ++ } ++ return fail("GRAPH_PROJECTION_FAILED", projected.error || "GRAPH_PROJECTION_FAILED"); ++} ++ ++function bindCanonicalInputs(input, bound) { ++ const provided = input.canonicalInputs; ++ if (!provided || typeof provided !== "object") return { ok: true }; ++ const graph = bound.executionGraph; ++ const contractDigest = graph.contract_digest || (input.contract && input.contract.contract_digest); ++ const mismatches = [ ++ ["contract_digest", provided.contract_digest, contractDigest], ++ ["policy_snapshot_id", provided.policy_snapshot_id, graph.policy_snapshot_id], ++ ["execution_graph_digest", provided.execution_graph_digest, graph.graph_id], ++ ]; ++ for (const [name, providedValue, boundValue] of mismatches) { ++ if (typeof providedValue === "string" && boundValue && providedValue !== boundValue) { ++ return fail("GRAPH_DIVERGENCE", `canonicalInputs.${name} does not match the bound graph/contract`); ++ } ++ } ++ return { ok: true }; ++} ++ ++function rejectStaleEvidence(input, bound, evidence, rawBytes) { ++ const predecessorId = bound.candidate && bound.candidate.predecessor_id; ++ const graph = input.priorAssuranceGraph; ++ if (predecessorId && !graph) { ++ return fail("STALE_EVIDENCE", "predecessor-bound candidate requires prior Assurance Graph"); ++ } ++ if (!graph) return { ok: true }; ++ if (assuranceGraph.isEvidenceTransitivelyInvalidated(graph, evidence.evidence_id)) { ++ return fail("STALE_EVIDENCE", "evidence is reachable through a transitive invalidates edge"); ++ } ++ if (predecessorId) { ++ const predecessorBoundId = computeEvidenceId({ ...evidence, candidate_id: predecessorId }, rawBytes); ++ const priorIds = new Set((graph.nodes || []).map((node) => node && node.id)); ++ if ( ++ priorIds.has(predecessorBoundId) || ++ assuranceGraph.isEvidenceTransitivelyInvalidated(graph, predecessorBoundId) ++ ) { ++ return fail("STALE_EVIDENCE", "reminted predecessor digest remains stale under invalidates"); ++ } ++ } ++ return { ok: true }; ++} ++ + /** + * Independently verify a frozen Candidate v2. + * Worker narrative is not authority. Evidence stays distinct from verdict. + * + * @param {object} input +- * @returns {{ ok: boolean, strategy?: string, evidence?: object[], verification?: object, reason_code?: string }} ++ * @returns {{ ok: boolean, strategy?: string, evidence?: object[], assessments?: object[], verification?: object, reason_code?: string }} + */ + function verifyCandidate(input) { + const bound = validateBindings(input); + if (!bound.ok) return bound; + ++ const canonicalBinding = bindCanonicalInputs(input, bound); ++ if (!canonicalBinding.ok) return canonicalBinding; ++ + const strategy = selectStrategy(input.declaredStrategy); + const rawList = Array.isArray(input.rawEvidence) ? input.rawEvidence : []; + const classified = []; + +- for (const raw of rawList) { +- const normalized = normalizeEvidence(raw, bound.candidate, bound.executionGraph); ++ for (let index = 0; index < rawList.length; index += 1) { ++ const raw = rawList[index]; ++ const channel = channelCollector(input, index); ++ const provenanceGate = resolveEvidenceProvenance(raw, channel); ++ if (!provenanceGate.ok) return provenanceGate; ++ const normalized = normalizeEvidence(raw, bound.candidate, bound.executionGraph, channel); + if (!normalized.ok) return normalized; +- if (input.priorAssuranceGraph && isEvidenceTransitivelyInvalidated(input.priorAssuranceGraph, normalized.evidence.evidence_id)) { +- return fail("STALE_EVIDENCE", "evidence is reachable through a transitive invalidates edge"); +- } ++ const stale = rejectStaleEvidence( ++ input, ++ bound, ++ normalized.evidence, ++ raw.bytes !== undefined ? raw.bytes : raw.rawBytes ++ ); ++ if (!stale.ok) return stale; + classified.push(normalized); + } + + const evaluated = evaluateStrategy(strategy, classified); + if (!evaluated.ok) return evaluated; + ++ const coverage = walkMustObligations({ ++ classified, ++ executionGraph: bound.executionGraph, ++ candidate: bound.candidate, ++ policySnapshotId: bound.executionGraph.policy_snapshot_id, ++ }); ++ if (!coverage.ok) return coverage; ++ + const evidenceRecords = classified.map((item) => item.evidence); + const hasNonRuntimeExtra = classified.some( + (item) => item.evidence.provenance === "external-unverified" || item.evidence.provenance === "human-decision" +@@ -52,7 +122,7 @@ function verifyCandidate(input) { + verdict, + }); + +- const projected = projectAssuranceGraph({ ++ const projected = assuranceGraph.projectAssuranceGraph({ + canonicalInputs: input.canonicalInputs || { + contract: input.contract, + sourceSnapshot: input.sourceSnapshot, +@@ -60,20 +130,22 @@ function verifyCandidate(input) { + candidate: bound.candidate, + executionGraph: bound.executionGraph, + evidence: classified, ++ assessments: coverage.assessments, + verification, + }); ++ if (!projected.ok) { ++ return mapProjectionFailure(projected); ++ } + +- const result = { ++ return { + ok: true, + strategy, + evidence: evidenceRecords, ++ assessments: coverage.assessments, + verification, ++ assurance_graph: projected.graph, ++ equivalence_manifest: assuranceGraph.emitEquivalenceManifest(projected.graph), + }; +- if (projected.ok) { +- result.assurance_graph = projected.graph; +- result.equivalence_manifest = emitEquivalenceManifest(projected.graph); +- } +- return result; + } + + module.exports = { +diff --git a/scripts/lib/independent-verifier/index.test.js b/scripts/lib/independent-verifier/index.test.js +index 4e2f56a..2dcbc8f 100644 +--- a/scripts/lib/independent-verifier/index.test.js ++++ b/scripts/lib/independent-verifier/index.test.js +@@ -11,6 +11,8 @@ const { computeTreeDigest } = require("../worker-workspace.js"); + const { verifyCandidate, selectStrategy } = require("./index.js"); + const { computeEvidenceId, digestRawBytes } = require("./evidence.js"); + const { computeVerificationId } = require("./verdict.js"); ++const { computeAssessmentId } = require("./assessment.js"); ++const assuranceGraph = require("../assurance-graph/index.js"); + + const ROOT = path.resolve(__dirname, "../../.."); + const CONFIG_PATH = path.join(ROOT, "openspec", "config.yaml"); +@@ -73,19 +75,39 @@ function buildHarness(overrides = {}) { + policySnapshot, + contract, + repository: { files }, ++ collector: Object.prototype.hasOwnProperty.call(overrides, "collector") ++ ? overrides.collector ++ : trustedCollector("runtime-observed"), + }; + } + ++function trustedCollector(provenance) { ++ if (provenance === "tool-produced") { ++ return { id: "tool-execution", transport: "tool-execution-transport" }; ++ } ++ if (provenance === "host-attested") { ++ return { id: "host-adapter", transport: "execution-transport" }; ++ } ++ if (provenance === "runtime-observed") { ++ return { id: "node-test", transport: "tool-execution-transport" }; ++ } ++ return undefined; ++} ++ + function raw(role, bytes, extra = {}) { +- return { ++ const provenance = extra.provenance || "runtime-observed"; ++ const record = { + role, + bytes, +- provenance: extra.provenance || "runtime-observed", ++ provenance, + origin: extra.origin || `role:${role}`, + node_id: extra.node_id || "repair-core", + obligation_ids: extra.obligation_ids || ["req-repair-001"], +- ...extra.fields, + }; ++ if (Object.prototype.hasOwnProperty.call(extra, "collector") && extra.collector) { ++ record.collector = extra.collector; ++ } ++ return { ...record, ...extra.fields }; + } + + function featureEvidence() { +@@ -328,6 +350,7 @@ test("REQ-independent-verification-003: runtime-observed satisfies; model-report + + const modelReported = verifyCandidate({ + ...harness, ++ collector: undefined, + declaredStrategy: "feature", + rawEvidence: featureEvidence().map((item) => ({ ...item, provenance: "model-reported" })), + }); +@@ -389,6 +412,29 @@ test("REQ-independent-verification-003: stale, foreign, or fabricated evidence i + assert.equal(staleDependent.verification, undefined); + }); + ++test("F-ad61b7e3cff9629a: predecessor remint without prior graph and digest reuse under invalidates are STALE", () => { ++ const files = { "src/index.js": "module.exports = 1;\n" }; ++ const predecessor = buildHarness({ files }); ++ const first = verifyCandidate({ ...predecessor, declaredStrategy: "feature", rawEvidence: featureEvidence() }); ++ assert.equal(first.ok, true, first.reason_code); ++ const successor = buildHarness({ ++ files, ++ diff_hash: "sha256:2222222222222222222222222222222222222222222222222222222222222222", ++ predecessorCandidate: predecessor.candidate, ++ }); ++ assert.equal(verifyCandidate({ ...successor, declaredStrategy: "feature", rawEvidence: featureEvidence() }).reason_code, "STALE_EVIDENCE"); ++ const reminted = verifyCandidate({ ++ ...successor, ++ declaredStrategy: "feature", ++ rawEvidence: featureEvidence(), ++ priorAssuranceGraph: { ++ ...first.assurance_graph, ++ edges: [...(first.assurance_graph.edges || []), { from: successor.candidate.candidate_id, relation: "invalidates", to: first.evidence[0].evidence_id }], ++ }, ++ }); ++ assert.equal(reminted.reason_code, "STALE_EVIDENCE"); ++}); ++ + test("REQ-independent-verification-004: sufficient evidence yields a verification verdict without embedding it in evidence", () => { + const harness = buildHarness(); + const result = verifyCandidate({ +@@ -406,9 +452,11 @@ test("REQ-independent-verification-004: sufficient evidence yields a verificatio + + test("REQ-independent-verification-004: extra human-decision evidence yields PASS WITH WARNINGS", () => { + const harness = buildHarness(); ++ const nodeTest = harness.collector; + const result = verifyCandidate({ + ...harness, + declaredStrategy: "feature", ++ collectors: [...featureEvidence().map(() => nodeTest), undefined], + rawEvidence: [ + ...featureEvidence(), + raw("annotation", "human reviewed", { provenance: "human-decision" }), +@@ -434,7 +482,7 @@ test("REQ-independent-verification-002: feature anyOf requires contract or integ + }); + + test("REQ-independent-verification-002: Strict TDD rejects host-attested red and green", () => { +- const harness = buildHarness(); ++ const harness = buildHarness({ collector: { id: "host-adapter", transport: "execution-transport" } }); + const result = verifyCandidate({ + ...harness, + rawEvidence: [ +@@ -502,3 +550,258 @@ test("REQ-independent-verification-003/004: evidence_id and verification_id are + computeVerificationId(first.verification.candidate_id, first.verification.verdict, first.verification.evidence_ids) + ); + }); ++ ++test("REQ-independent-verification-003: payload runtime-observed without collector fails UNTRUSTED_COLLECTOR", () => { ++ const harness = buildHarness({ collector: undefined }); ++ const result = verifyCandidate({ ++ ...harness, ++ declaredStrategy: "feature", ++ rawEvidence: featureEvidence(), ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "UNTRUSTED_COLLECTOR"); ++ assert.equal(Object.prototype.hasOwnProperty.call(result, "verification"), false); ++}); ++ ++test("REQ-independent-verification-003: allowlisted node-test collector derives runtime-observed", () => { ++ const harness = buildHarness(); ++ const result = verifyCandidate({ ++ ...harness, ++ declaredStrategy: "feature", ++ rawEvidence: featureEvidence(), ++ }); ++ assert.equal(result.ok, true, result.error || result.reason_code); ++ assert.equal(result.evidence.every((ev) => ev.provenance === "runtime-observed"), true); ++ assert.equal(result.evidence.every((ev) => !Object.prototype.hasOwnProperty.call(ev, "collector")), true); ++}); ++ ++test("REQ-independent-verification-003: worker collector is model-reported and insufficient for runtime MUST", () => { ++ const harness = buildHarness({ collector: { id: "worker", transport: "worker-transport" } }); ++ const result = verifyCandidate({ ++ ...harness, ++ declaredStrategy: "feature", ++ rawEvidence: featureEvidence().map((item) => ({ ...item, provenance: "model-reported" })), ++ }); ++ assert.equal(result.ok, false); ++ assert.ok(["INSUFFICIENT_PROVENANCE", "UNTRUSTED_COLLECTOR"].includes(result.reason_code)); ++}); ++ ++test("REQ-independent-verification-003: payload strong vs collector weak fails closed", () => { ++ const harness = buildHarness({ collector: { id: "worker", transport: "worker-transport" } }); ++ const result = verifyCandidate({ ++ ...harness, ++ declaredStrategy: "feature", ++ rawEvidence: featureEvidence(), ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "UNTRUSTED_COLLECTOR"); ++}); ++ ++test("F-d5739d79237afeb8/F-2fc6db350f5b8afc: weak+allowlisted fails closed; mapping and npm-test/node:test", () => { ++ const harness = buildHarness(); ++ const run = (rawEvidence, collector) => verifyCandidate({ ++ ...harness, ++ collector, ++ declaredStrategy: "feature", ++ rawEvidence, ++ }); ++ const nodeTest = { id: "node-test", transport: "tool-execution-transport" }; ++ for (const provenance of ["model-reported", "human-decision", "external-unverified"]) { ++ const result = run(featureEvidence().map((item) => ({ ...item, provenance })), nodeTest); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "UNTRUSTED_COLLECTOR"); ++ } ++ assert.equal(run(featureEvidence(), { id: "tool-execution", transport: "tool-execution-transport" }).reason_code, "UNTRUSTED_COLLECTOR"); ++ assert.equal(run(featureEvidence(), { id: "node-test", transport: "execution-transport" }).reason_code, "UNTRUSTED_COLLECTOR"); ++ for (const id of ["npm-test", "node:test"]) { ++ const result = run(featureEvidence(), { id, transport: "tool-execution-transport" }); ++ assert.equal(result.ok, true, result.reason_code); ++ assert.equal(result.evidence.every((ev) => ev.provenance === "runtime-observed"), true); ++ } ++}); ++ ++test("F-d5739d79237afeb8: envelope collector fails closed; harness collector derives class", () => { ++ const h = { ...buildHarness(), declaredStrategy: "feature", collector: { id: "node-test", transport: "tool-execution-transport" } }; ++ assert.equal(verifyCandidate({ ...h, rawEvidence: featureEvidence().map((i) => ({ ...i, collector: h.collector })) }).reason_code, "UNTRUSTED_COLLECTOR"); ++ const ok = verifyCandidate({ ...h, rawEvidence: featureEvidence() }); ++ assert.equal(ok.ok && ok.evidence.every((e) => e.provenance === "runtime-observed"), true, ok.reason_code); ++}); ++ ++test("REQ-independent-verification-005: MUST without bound evidence fails UNFULFILLED_MUST after strategy", () => { ++ const harness = buildHarness(); ++ const result = verifyCandidate({ ++ ...harness, ++ declaredStrategy: "feature", ++ rawEvidence: featureEvidence().map((item) => ({ ...item, obligation_ids: [] })), ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "UNFULFILLED_MUST"); ++ assert.match(result.error || "", /req-repair-001/); ++ assert.equal(Object.prototype.hasOwnProperty.call(result, "verification"), false); ++}); ++ ++test("REQ-independent-verification-005: alien obligation_id fails UNKNOWN_OBLIGATION_ID", () => { ++ const harness = buildHarness(); ++ const result = verifyCandidate({ ++ ...harness, ++ declaredStrategy: "feature", ++ rawEvidence: featureEvidence().map((item, index) => ++ index === 0 ? { ...item, obligation_ids: ["not-in-manifest"] } : item ++ ), ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "UNKNOWN_OBLIGATION_ID"); ++}); ++ ++test("REQ-independent-verification-005: evidence on a non-implementing node fails WRONG_IMPLEMENTING_NODE", () => { ++ const extraNode = { ++ node_id: "other-node", ++ kind: "repair-action/v1", ++ operation: "apply_repair_patch", ++ objective: "Other", ++ dependencies: [], ++ ownership: { owner: "agent:repair", mode: "exclusive" }, ++ allowed_paths: ["src/other.js"], ++ invariants: ["inv-fail-closed"], ++ required_evidence: ["ev:test-pass"], ++ budget_ref: "budget:default", ++ }; ++ const files = { "src/index.js": "module.exports = 1;\n", "src/other.js": "module.exports = 2;\n" }; ++ const harness = buildHarness({ files }); ++ const { compileExecutionGraph, createPolicySnapshot } = require("../execution-graph/index.js"); ++ const policySnapshot = createPolicySnapshot({ effectiveRules: ["rule-fail-closed"] }); ++ const executionGraph = compileExecutionGraph({ ++ contract: harness.contract, ++ policySnapshot, ++ nodes: [...SAMPLE_NODES, extraNode], ++ obligations: SAMPLE_OBLIGATIONS, ++ }); ++ const result = verifyCandidate({ ++ ...harness, ++ executionGraph, ++ policySnapshot, ++ declaredStrategy: "feature", ++ rawEvidence: featureEvidence().map((item) => ({ ...item, node_id: "other-node" })), ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "WRONG_IMPLEMENTING_NODE"); ++}); ++ ++test("REQ-independent-verification-005: approved deferral skips MUST coverage", () => { ++ const deferred = [ ++ { ++ id: "req-repair-001", ++ criticality: "must", ++ implemented_by: ["repair-core"], ++ required_evidence: ["ev:test-pass"], ++ deferred: { reason: "Hardening deferred with maintainer approval", approved_by: "maintainer" }, ++ }, ++ ]; ++ const harness = buildHarness(); ++ const { compileExecutionGraph, createPolicySnapshot } = require("../execution-graph/index.js"); ++ const policySnapshot = createPolicySnapshot({ effectiveRules: ["rule-fail-closed"] }); ++ const contract = { ...harness.contract, obligations: deferred }; ++ const executionGraph = compileExecutionGraph({ ++ contract, ++ policySnapshot, ++ nodes: SAMPLE_NODES, ++ obligations: deferred, ++ }); ++ const result = verifyCandidate({ ++ ...harness, ++ contract, ++ executionGraph, ++ policySnapshot, ++ declaredStrategy: "feature", ++ rawEvidence: featureEvidence().map((item) => ({ ...item, obligation_ids: [] })), ++ }); ++ assert.equal(result.ok, true, result.error || result.reason_code); ++}); ++ ++test("REQ-independent-verification-007: projector failure is GRAPH_PROJECTION_FAILED without PASS or graph", () => { ++ const harness = buildHarness(); ++ const original = assuranceGraph.projectAssuranceGraph; ++ const run = (reason_code) => { ++ assuranceGraph.projectAssuranceGraph = () => ({ ok: false, reason_code, error: "stub" }); ++ return verifyCandidate({ ...harness, declaredStrategy: "feature", rawEvidence: featureEvidence() }); ++ }; ++ try { ++ const failed = run("GRAPH_PROJECTION_FAILED"); ++ assert.equal(failed.ok, false); ++ assert.equal(failed.reason_code, "GRAPH_PROJECTION_FAILED"); ++ assert.equal(Object.prototype.hasOwnProperty.call(failed, "assurance_graph"), false); ++ assert.equal(Object.prototype.hasOwnProperty.call(failed, "verification"), false); ++ const diverged = run("GRAPH_DIVERGENCE"); ++ assert.equal(diverged.ok, false); ++ assert.equal(diverged.reason_code, "GRAPH_DIVERGENCE"); ++ assert.equal(Object.prototype.hasOwnProperty.call(diverged, "verification"), false); ++ } finally { ++ assuranceGraph.projectAssuranceGraph = original; ++ } ++}); ++ ++test("F-6b1f8c8265c82b3e: mismatched canonicalInputs fail closed", () => { ++ const harness = buildHarness(); ++ const mismatch = "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; ++ for (const canonicalInputs of [{ contract_digest: mismatch }, { policy_snapshot_id: mismatch }, { execution_graph_digest: mismatch }]) { ++ const result = verifyCandidate({ ...harness, declaredStrategy: "feature", rawEvidence: featureEvidence(), canonicalInputs }); ++ assert.equal(result.ok, false); ++ assert.ok(["GRAPH_DIVERGENCE", "BINDING_MISMATCH"].includes(result.reason_code)); ++ assert.equal(Object.prototype.hasOwnProperty.call(result, "verification"), false); ++ } ++}); ++ ++test("REQ-independent-verification-004: strategy failure short-circuits without MUST upgrade", () => { ++ const harness = buildHarness(); ++ const result = verifyCandidate({ ++ ...harness, ++ declaredStrategy: "feature", ++ rawEvidence: [ ++ raw("acceptance", "acceptance"), ++ raw("invariants", "invariants"), ++ raw("contract", "contract"), ++ ], ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "MISSING_NEGATIVE"); ++ assert.notEqual(result.reason_code, "UNFULFILLED_MUST"); ++}); ++ ++test("REQ-independent-verification-006: four roles over one observation yield four assessments and one evidence_id", () => { ++ const harness = buildHarness(); ++ const shared = { origin: "shared-observation", obligation_ids: ["req-repair-001"] }; ++ const bytes = "shared-bytes"; ++ const result = verifyCandidate({ ++ ...harness, ++ declaredStrategy: "feature", ++ rawEvidence: [ ++ raw("acceptance", bytes, shared), ++ raw("invariants", bytes, shared), ++ raw("contract", bytes, shared), ++ 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, ++ }) ++ ); ++}); +diff --git a/scripts/lib/k1-scope-guard.test.js b/scripts/lib/k1-scope-guard.test.js +index 4b19e28..22e421a 100644 +--- a/scripts/lib/k1-scope-guard.test.js ++++ b/scripts/lib/k1-scope-guard.test.js +@@ -204,6 +204,7 @@ const SUCCESSOR_K2_PREFIXES = [ + "schemas/kernel/verification/fixtures/valid/v2-", + "schemas/kernel/verification/fixtures/invalid/v2-", + "schemas/kernel/assurance-graph/", ++ "schemas/kernel/assessment/", + ]; + + const PROTECTED_BASELINE_PATHS = [ +diff --git a/scripts/lib/k2a-maturity-docs.test.js b/scripts/lib/k2a-maturity-docs.test.js +index 1586bdb..5750c32 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/); +diff --git a/scripts/lib/k6b-schema-fixtures.test.js b/scripts/lib/k6b-schema-fixtures.test.js +index 83bc8fc..19ef9e3 100644 +--- a/scripts/lib/k6b-schema-fixtures.test.js ++++ b/scripts/lib/k6b-schema-fixtures.test.js +@@ -41,6 +41,13 @@ test("K6b schema registration: manifest indexes evidence/v2, verification/v2, an + assert.equal(verificationV2.$id, "ospec://schemas/kernel/verification/v2"); + const graph = loadSchemaById("ospec://schemas/kernel/assurance-graph/v1", { rootDir: ROOT }); + assert.equal(graph.$id, "ospec://schemas/kernel/assurance-graph/v1"); ++ ++ assert.ok(manifest.families.assessment, "manifest must register assessment"); ++ assert.equal(manifest.families.assessment.schema_version, 1); ++ assert.equal(manifest.families.assessment.$id, "ospec://schemas/kernel/assessment/v1"); ++ assert.equal(manifest.families.assessment.path, "schemas/kernel/assessment/v1.schema.json"); ++ const assessment = loadSchemaById("ospec://schemas/kernel/assessment/v1", { rootDir: ROOT }); ++ assert.equal(assessment.$id, "ospec://schemas/kernel/assessment/v1"); + }); + + test("K6b contract claims: additive families list required fields without replacing v1 claims", () => { +@@ -86,6 +93,19 @@ test("K6b contract claims: additive families list required fields without replac + "derived-from", + "invalidates", + ]); ++ ++ assert.ok(claims.families.assessment, "assessment claims must exist"); ++ assert.deepEqual(claims.families.assessment.required_fields, [ ++ "schema_version", ++ "kind", ++ "assessment_id", ++ "evidence_id", ++ "role", ++ "obligation_id", ++ "node_id", ++ "candidate_id", ++ "policy_snapshot_id", ++ ]); + }); + + test("K6b evidence/v2: valid fixture passes; verdict and unknown provenance fail closed", () => { +@@ -216,3 +236,60 @@ test("K6b: K1 evidence/v1 and verification/v1 files and pins remain byte-identic + const result = assertK1SchemasUnchanged(ROOT); + assert.equal(result.ok, true, `K1 baseline must be intact: ${JSON.stringify(result)}`); + }); ++ ++test("K6b assessment/v1: valid fixture passes; verdict, missing fields, and cross-family fail closed", () => { ++ const assessmentSchema = loadSchemaById("ospec://schemas/kernel/assessment/v1", { rootDir: ROOT }); ++ const evidenceSchema = loadSchemaById("ospec://schemas/kernel/evidence/v2", { rootDir: ROOT }); ++ const verificationSchema = loadSchemaById("ospec://schemas/kernel/verification/v2", { rootDir: ROOT }); ++ ++ const complete = readJson("schemas/kernel/assessment/fixtures/valid/v1-complete.json"); ++ const completeRes = validateInstance(assessmentSchema, complete); ++ assert.equal(completeRes.valid, true, `valid assessment rejected: ${JSON.stringify(completeRes.errors)}`); ++ ++ const missing = readJson("schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json"); ++ assert.equal(validateInstance(assessmentSchema, missing).valid, false); ++ ++ const withVerdict = readJson("schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json"); ++ const verdictRes = validateInstance(assessmentSchema, withVerdict); ++ assert.equal(verdictRes.valid, false, "assessment/v1 with verdict must fail"); ++ assert.ok(verdictRes.errors.some((e) => /verdict|additionalProperties/i.test(e.message + e.path + e.rule))); ++ ++ const alias = readJson("schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json"); ++ assert.equal(validateInstance(assessmentSchema, alias).valid, false, "evidence/v2 must not validate as assessment/v1"); ++ assert.equal(validateInstance(evidenceSchema, complete).valid, false, "assessment/v1 must not validate as evidence/v2"); ++ assert.equal(validateInstance(verificationSchema, complete).valid, false, "assessment/v1 must not validate as verification/v2"); ++}); ++ ++test("K6b assessment/v1: four roles share one evidence_id and produce distinct assessment_id values", () => { ++ const schema = loadSchemaById("ospec://schemas/kernel/assessment/v1", { rootDir: ROOT }); ++ const four = readJson("schemas/kernel/assessment/fixtures/valid/v1-four-roles.json"); ++ assert.ok(Array.isArray(four), "v1-four-roles.json must be an array of payloads"); ++ assert.equal(four.length, 4); ++ ++ const evidenceIds = new Set(); ++ const assessmentIds = new Set(); ++ const roles = new Set(); ++ for (const payload of four) { ++ const result = validateInstance(schema, payload); ++ assert.equal(result.valid, true, `four-role payload rejected: ${JSON.stringify(result.errors)}`); ++ evidenceIds.add(payload.evidence_id); ++ assessmentIds.add(payload.assessment_id); ++ roles.add(payload.role); ++ } ++ assert.equal(evidenceIds.size, 1, "four roles must share one evidence_id"); ++ assert.equal(assessmentIds.size, 4, "four roles must produce four assessment_id values"); ++ assert.equal(roles.size, 4); ++}); ++ ++test("K6b: evidence/v2 and verification/v2 schema bytes remain frozen after assessment publication", () => { ++ const V2_PINS = { ++ "schemas/kernel/evidence/v2.schema.json": ++ "sha256:fad66198ac48f47109041e45017e77227268610cddbb929e4dfcc3e0c5ec4910", ++ "schemas/kernel/verification/v2.schema.json": ++ "sha256:441ee351d7c094558818a3af0cfcac8b823818e5562c341d3595f2305cc4396b", ++ }; ++ for (const [rel, expected] of Object.entries(V2_PINS)) { ++ const actual = digestFile(path.join(ROOT, ...rel.split("/"))); ++ assert.equal(actual, expected, `${rel} bytes must remain frozen`); ++ } ++}); +diff --git a/scripts/lib/lifecycle-kernel/k1-compat.js b/scripts/lib/lifecycle-kernel/k1-compat.js +index ea83a61..63442c0 100644 +--- a/scripts/lib/lifecycle-kernel/k1-compat.js ++++ b/scripts/lib/lifecycle-kernel/k1-compat.js +@@ -177,6 +177,7 @@ function listK1SchemaFiles(rootDir) { + "schemas/kernel/verification/fixtures/valid/v2-", + "schemas/kernel/verification/fixtures/invalid/v2-", + "schemas/kernel/assurance-graph/", ++ "schemas/kernel/assessment/", + // Evolutionary catalog registries (excluded from K1 frozen baseline pin) + "schemas/kernel/manifest.json", + "schemas/kernel/contract-claims.json", +diff --git a/openspec/changes/k6b-verification-integrity-remediation/apply-progress.md b/openspec/changes/k6b-verification-integrity-remediation/apply-progress.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/apply-progress.md +@@ -0,0 +1,99 @@ ++# Apply Progress: k6b-verification-integrity-remediation ++ ++**Mode:** Focused TDD (`testing.tdd_mode: focused`) ++**Delivery:** `size:exception` (maintainer-accepted oversized single PR; Phases 1–8 in one batch) ++**Batch:** 1 (first apply; no prior progress) ++**Branch:** `feat/k6b-verifier-evidence-assurance-graph` ++**Verified at:** 2026-08-27T14:24:10Z ++ ++## Batch 1 — Phases 1–8 (complete) ++ ++All 30 tasks implemented and locally verified. ++ ++### Local verification ++ ++```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 \ ++ scripts/lib/k2a-maturity-docs.test.js ++→ 65 pass, 0 fail ++ ++node --test scripts/lib/contract-checkers/k1-schema-compat.test.js \ ++ scripts/lib/k1-scope-guard.test.js \ ++ scripts/lib/kernel-schema-fixtures.test.js \ ++ scripts/lib/roadmap-boundary.test.js ++→ 34 pass, 0 fail ++``` ++ ++v2/K1 schema pins remain byte-identical (`evidence/v2`, `verification/v2`, K1 baseline). ++ ++### TDD Cycle Evidence ++ ++| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR | Notes / Rationale | ++| ---- | --- | ----- | ---- | --- | ----- | ----- | ----- | ----- | ++| 1.1–1.5 | `scripts/lib/k6b-schema-fixtures.test.js` | contract | schema fixtures + digest pins | [x] | [x] | [x] | [x] | assessment/v1 family; four-role distinct ids; v2/K1 pins frozen | ++| 2.1–2.3 | `scripts/lib/independent-verifier/assessment.test.js` | unit | schema validate | [x] | [x] | [x] | [x] | `computeAssessmentId` includes role+obligation; verdict rejected | ++| 3.1–3.4 | `scripts/lib/independent-verifier/index.test.js` | unit | allowlist mapper | [x] | [x] | [x] | [x] | payload-only strong → `UNTRUSTED_COLLECTOR`; collector ≠ digest | ++| 4.1–4.3 | `scripts/lib/independent-verifier/obligation-coverage.test.js` | unit | MUST walk | [x] | [x] | [x] | [x] | `UNFULFILLED_MUST` / `UNKNOWN_OBLIGATION_ID` / `WRONG_IMPLEMENTING_NODE`; empty `required_evidence` fails | ++| 4R S2 | `scripts/lib/independent-verifier/obligation-coverage.test.js` | unit | 6 pass | lock | [x] | [x] | [x] | `INSUFFICIENT_PROVENANCE`; 2nd MUST; deferral incompleto; grafo ausente `BINDING_MISMATCH` | ++| 5.1–5.4 | `scripts/lib/independent-verifier/index.test.js` | integration | facade stub | [x] | [x] | [x] | [x] | projector stub → `GRAPH_PROJECTION_FAILED`; unique-sort still one E | ++| 6.1–6.5 | `scripts/lib/assurance-graph/index.test.js` | unit | canonical `graph_id` | [x] | [x] | [x] | [x] | kind/namespace `rejectForbidden`; replay byte-identical; C1→C2 → `GRAPH_DIVERGENCE` | ++| 7.1–7.4 | `scripts/k6b-verifier-assurance-graph-e2e.test.js` | e2e | persistable replay | [x] | [x] | [x] | [x] | assessments + `canonical_inputs` persisted; adversarial cases in index tests | ++| 8.1–8.2 | `scripts/lib/k2a-maturity-docs.test.js` | docs | status table | [x] | [x] | n/a | n/a | K6b `revise`; K6c `blocked-by-K6b-remediation` | ++ ++### Task status ++ ++- [x] 1.1–1.5 Assessment/v1 schema publication ++- [x] 2.1–2.3 Assessment identity module ++- [x] 3.1–3.4 Collector provenance allowlist ++- [x] 4.1–4.3 Obligation Manifest MUST walk ++- [x] 5.1–5.4 Verifier facade integration ++- [x] 6.1–6.5 Assurance Graph remediation ++- [x] 7.1–7.4 Adversarial tests and E2E ++- [x] 8.1–8.2 Roadmap documentation ++ ++### Deviations from design ++ ++None — implementation matches design. Companion updates required by existing tests: ++ ++- `v1-four-roles.json` is an array of four payloads; `k1-schema-compat` skips it via `fixtureNameFilter` (same pattern as `k3-frozen.json`) so the generic object walker does not treat the bundle as one instance. ++- `schemas/kernel/assessment/` excluded from K1 frozen inventory (`k1-compat.js`, `k1-scope-guard.test.js` successor prefixes). ++- `k2a-maturity-docs.test.js` updated to pin K6b `revise` / K6c `blocked-by-K6b-remediation`. ++ ++### Issues found ++ ++None blocking. Existing verifier/E2E tests that claimed strong provenance without collector now supply an allowlisted collector (or omit collector to assert `UNTRUSTED_COLLECTOR`). ++ ++## Batch 2 — 4R slice `S-ea4088e8a61de9f8` (collector-trust-boundary) ++ ++**Lineage:** `sha256:262dda4ab0b3ec0fe60b7db34683c55d3c4d2590fe69282c40197cbc95aacf42` ++**Delivery:** `exception-ok` / this slice only. Forecast cap 180. ++ ++Closed `F-d5739d79237afeb8` (CRITICAL): weak claim + allowlisted collector now fails `UNTRUSTED_COLLECTOR`; claim↔collector disagreement is symmetric. ++ ++Co-located WARNINGs remediated in-cap: collector mapping tests (`F-2fc6db350f5b8afc`); STALE remint (`F-ad61b7e3cff9629a`); canonicalInputs bind (`F-6b1f8c8265c82b3e`); GRAPH_DIVERGENCE facade (`F-990aa817913b8273`); `rejectForbidden` kind-first + comments (`F-99f9c70bdae46c12`); projector satisfies comment (`F-eb2d325d6d801a14`); documented `derived` vs collector resolution (`F-4839a9a36f0b55be`). ++ ++### Local verification ++ ++```text ++node --test scripts/lib/independent-verifier/index.test.js \ ++ scripts/lib/assurance-graph/index.test.js \ ++ scripts/k6b-verifier-assurance-graph-e2e.test.js ++→ 48 pass, 0 fail ++``` ++ ++## Batch 3 — 4R retry 2 `S-ea4088e8a61de9f8` ++- [x] F-d5739d79237afeb8: envelope `collector` fails closed; harness `input.collector` derives class. GREEN: `node --test --test-name-pattern "envelope collector fails closed" scripts/lib/independent-verifier/index.test.js` ++ ++## Batch 4 — 4R `S-ad5558b5639b6890` ++- [x] F-b3d6518c12aa69fe / F-00f97ff647d28eea / F-ef73f7e16cab6436 + colocated F-2be19c4683d81ba1 / F-9d6a187e3d18dbf5 ++- Safety net 6 pass; after 10 pass / 0 fail: `node --test scripts/lib/independent-verifier/obligation-coverage.test.js` ++ ++## Batch 5 — successor `new-candidate` (helpers + remaining WARNINGs) ++- [x] Harness `input.collector` (optional `input.collectors[]`); envelope `collector` still fail-closed ++- [x] F-2fc6db350f5b8afc mapping `npm-test`/`node:test`; F-4839a9a36f0b55be `provenanceClass`; F-f979f00ae92cda6f comment ++- GREEN: `node --test scripts/lib/independent-verifier/index.test.js scripts/lib/independent-verifier/obligation-coverage.test.js scripts/lib/assurance-graph/index.test.js scripts/k6b-verifier-assurance-graph-e2e.test.js` → 59 pass / 0 fail +diff --git a/openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md b/openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/decisions/adr-001.md +@@ -0,0 +1,23 @@ ++# ADR-001: Additive assessment family `$id` ++ ++- Status: proposed ++- Change: k6b-verification-integrity-remediation ++- Date: 2026-08-27 ++ ++## Context ++ ++Assumption `sdd-propose-001` left the persistable binding `$id` to design. Intent forbids in-place mutation of `evidence/v2` and K1 v1. Assessment identity must include `role` and `obligation_id` without collapsing physical `EvidenceId`. ++ ++## Decision ++ ++Publish family `assessment` at `$id` `ospec://schemas/kernel/assessment/v1`, `kind: "assessment/v1"`, path `schemas/kernel/assessment/v1.schema.json`, `schema_version: 1`. Register additively in `manifest.json` and `contract-claims.json`. Keep `evidence/v2`, `verification/v2`, and K1 v1 bytes/pins frozen. ++ ++## Alternatives ++ ++- Mutate `evidence/v2` to carry role/obligation: rejected; observation identity would absorb evaluation identity. ++- Evolve `verification/v3` with embedded bindings: rejected; verdict unique-sort of `evidence_ids` still hides distinct roles. ++- `$id` `ospec://schemas/kernel/assessment-binding/v1`: rejected; breaks the `{noun}/vN` kernel `$id` pattern. ++ ++## Consequences ++ ++Consumers pin a third K6b-era family without migrating evidence. Rollback deletes the additive family. Four roles over one observation become four `assessment_id` values by construction. +diff --git a/openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md b/openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/decisions/adr-002.md +@@ -0,0 +1,23 @@ ++# ADR-002: MUST walk after strategy; assessment identity ≠ EvidenceId ++ ++- Status: proposed ++- Change: k6b-verification-integrity-remediation ++- Date: 2026-08-27 ++ ++## Context ++ ++`evaluateStrategy()` only checks role minimums. PASS was therefore possible without Obligation Manifest coverage. `obligation_ids` on classified evidence are ephemeral, so `satisfies` edges cannot be replayed from `evidence/v2`. ++ ++## Decision ++ ++Keep `evaluateStrategy` as the role-shape gate. After it succeeds, walk every non-deferred `must` obligation, emit `assessment/v1` records, and require those assessments for PASS. `computeEvidenceId` stays observation-only. `assessment_id` fingerprints `evidence_id`, `role`, `obligation_id`, `node_id`, `candidate_id`, and `policy_snapshot_id`. ++ ++## Alternatives ++ ++- Fold MUST coverage into `evaluateStrategy`: rejected; mixes strategy policy with graph obligations and reason codes. ++- Walk obligations before strategy: rejected; delays cheap role failures. ++- Treat unique-sort of `verification.evidence_ids` as binding identity: rejected; one EvidenceId used as four roles would collapse. ++ ++## Consequences ++ ++Reason codes split cleanly (`MISSING_STRATEGY_MINIMUM` vs `UNFULFILLED_MUST` / `UNKNOWN_OBLIGATION_ID` / `WRONG_IMPLEMENTING_NODE`). Projector `satisfies` edges consume assessments only. Existing tests that omit obligation bindings will fail closed until updated. +diff --git a/openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md b/openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/decisions/adr-003.md +@@ -0,0 +1,23 @@ ++# ADR-003: Strong provenance from collector/transport allowlist ++ ++- Status: proposed ++- Change: k6b-verification-integrity-remediation ++- Date: 2026-08-27 ++ ++## Context ++ ++`normalizeEvidence` currently copies `raw.provenance` into `evidence/v2`. A worker can claim `runtime-observed`. PKI is out of scope. Payload digest identifies bytes, not origin. ++ ++## Decision ++ ++Derive strong classes (`runtime-observed`, `host-attested`, `tool-produced`) from harness-supplied `collector.id` + `transport` via a fail-closed allowlist (`node-test`/`npm-test`/`node:test` → runtime-observed; `tool-execution` → tool-produced; `host-adapter` → host-attested). Worker/absent/unknown collectors cannot produce a strong class. Store the derived class on `evidence/v2`; never copy collector metadata onto that record. ++ ++## Alternatives ++ ++- Trust the payload provenance string: rejected; this is the verified defect. ++- Require signatures or PKI: rejected; out of scope for this change. ++- Treat payload digest as origin: rejected; digest is content identity only. ++ ++## Consequences ++ ++Callers that claim a strong class must pass allowlisted collector metadata (tests included). Mis-mapped collector ids fail closed rather than silently weakening provenance. +diff --git a/openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md b/openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/decisions/adr-004.md +@@ -0,0 +1,23 @@ ++# ADR-004: Canonical graph_id fingerprint and fail-closed projection ++ ++- Status: proposed ++- Change: k6b-verification-integrity-remediation ++- Date: 2026-08-27 ++ ++## Context ++ ++`graph_id` currently hashes only `candidate_id` plus canonical nodes/edges. Contract or policy changes do not diverge. `verifyCandidate` returns `ok: true` when projection fails, omitting the graph. `rejectForbidden` substring-matches `id`, so `REQ-add-authorization-header` is rejected. ++ ++## Decision ++ ++Fingerprint `contract_digest`, `policy_snapshot_id`, Execution Graph digest, canonical OpenSpec input, `candidate_id`, and canonical nodes/edges in `graph_id`. Persist those digests as optional `canonical_inputs` on `assurance-graph/v1`. `verifyCandidate` requires a successful projection before `ok: true`. Match forbidden subjects by `kind`/`namespace`, never by `id` substring. ++ ++## Alternatives ++ ++- Keep the current `graph_id` preimage: rejected; replay cannot detect contract/policy drift. ++- Leave projection optional on the facade: rejected; PASS without a graph is the integrity hole. ++- Continue substring markers on `id`: rejected; legitimate requirement ids containing `authorization` fail. ++ ++## Consequences ++ ++Existing runtime `graph_id` values change. Replay becomes possible from persistable outputs. Facade tests that ignored projector failure must assert `GRAPH_PROJECTION_FAILED` / `GRAPH_DIVERGENCE`. +diff --git a/openspec/changes/k6b-verification-integrity-remediation/design.md b/openspec/changes/k6b-verification-integrity-remediation/design.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/design.md +@@ -0,0 +1,210 @@ ++# Design: k6b-verification-integrity-remediation ++ ++## Technical Approach ++ ++Remediate six K6b integrity defects in the existing CommonJS verifier and projector. Do not reopen the macro-slice. `evaluateStrategy()` stays a role-shape gate. A new MUST walk over the Obligation Manifest, persistable `assessment/v1` bindings, collector-derived provenance, canonical `graph_id` fingerprinting, fail-closed projection, and kind/namespace `rejectForbidden` close the gaps. `evidence/v2` remains the physical observation; `verification/v2` remains the verdict. Roadmap status is a documentation edit only. ++ ++Resolves assumption `sdd-propose-001` via ADR-001. ++ ++## Architecture Decisions ++ ++### Decision: Additive assessment family `$id` (ADR-001) ++ ++| Option | Tradeoff | Decision | ++| --- | --- | --- | ++| Mutate `evidence/v2` with `role` / `obligation_id` | Collapses observation identity; forbidden by intent. | Rejected | ++| Evolve `verification/v3` to embed bindings | Unique-sort of `evidence_ids` still hides roles; mutates verdict family. | Rejected | ++| `ospec://schemas/kernel/assessment-binding/v1` | Explicit but breaks `{noun}/vN` `$id` pattern. | Rejected | ++| `ospec://schemas/kernel/assessment/v1` | Additive, pinnable, distinct `kind`. | **Chosen** | ++ ++**Rationale**: Matches `evidence/v2` and `verification/v2`. Manifest key `assessment` (first version, like `assurance-graph`). `kind: "assessment/v1"`. Path `schemas/kernel/assessment/v1.schema.json`. ++ ++### Decision: MUST walk after strategy, assessments ≠ EvidenceId (ADR-002) ++ ++| Option | Tradeoff | Decision | ++| --- | --- | --- | ++| Fold obligations into `evaluateStrategy` | Mixes role policy with graph coverage; harder to reason codes. | Rejected | ++| Walk MUST first | Wastes work when strategy already fails. | Rejected | ++| Strategy then MUST walk; assessment id includes role+obligation | Fail-fast role check; persistable coverage. | **Chosen** | ++ ++**Rationale**: Strategy satisfied ≠ Execution Graph satisfied. `computeEvidenceId` is unchanged (no role). Assessment identity fingerprints role, obligation, node, evidence, candidate, and policy snapshot. ++ ++### Decision: Collector/transport allowlist, no PKI (ADR-003) ++ ++| Option | Tradeoff | Decision | ++| --- | --- | --- | ++| Trust payload `provenance` | Current defect; worker can claim `runtime-observed`. | Rejected | ++| Require signatures / PKI | Out of scope. | Rejected | ++| Harness-supplied collector+transport allowlist | Fail-closed; digest ≠ origin. | **Chosen** | ++ ++**Rationale**: Strong class is derived from the channel the harness invoked. Unknown or absent collector cannot produce a strong class. ++ ++### Decision: Canonical `graph_id` + facade fail-closed (ADR-004) ++ ++| Option | Tradeoff | Decision | ++| --- | --- | --- | ++| Keep `graph_id` = candidate+nodes+edges | Contract/policy drift is invisible. | Rejected | ++| Return `ok: true` without a graph | Current facade defect. | Rejected | ++| Fingerprint canonicalInputs; `ok: true` requires projection | Replayable; fail-closed. | **Chosen** | ++ ++**Rationale**: `satisfies` edges rebuild from persistable assessments, never from ephemeral `obligation_ids`. ++ ++## Data Flow ++ ++### Verification sequence (MUST walk vs strategy) ++ ++```text ++Caller verifyCandidate strategy-policy obligation-coverage projector ++ | verify(input) | | | | ++ |---------------------->| validateBindings | | | ++ | | selectStrategy------->| | | ++ | | normalizeEvidence | collector → class | | ++ | | evaluateStrategy----->| roles only | | ++ | | | ok ≠ graph covered | | ++ | | walk MUST-------------------------------------->| | ++ | | | skip approved deferred | | ++ | | | emit assessment/v1 | | ++ | | projectAssuranceGraph------------------------------------------------>| ++ | | | graph_id includes canonicalInputs | ++ | | | satisfies ← assessments only | ++ | | if !projected.ok → ok:false, no PASS, no graph | ++ | | emit verification/v2 (unique-sort evidence_ids still one E) | ++ |<----------------------| evidence, assessments, verification, assurance_graph | ++``` ++ ++**Order**: bindings → strategy selection → normalize (collector class) → `evaluateStrategy` → MUST walk → project → (optional reconcile) → emit verdict. Strategy failure short-circuits; MUST walk never upgrades a failed strategy. ++ ++**Deferred**: skip a `must` item only when `deferred.reason` and `deferred.approved_by` are both non-empty (same predicate as K4a `obligation-manifest.js`). Partial deferral is not deferred. `should` / `may` are not required for PASS. ++ ++**Coverage unit**: a non-deferred MUST is satisfied when ≥1 persistable assessment binds admissible evidence to that `obligation_id` and a `node_id` ∈ `implemented_by`. Join key is `obligation_id`, not echoing K4a tokens such as `ev:test-pass` onto `evidence/v2`. Any binding whose `node_id` is outside `implemented_by` fails closed even if another binding is valid. Empty `required_evidence` on a non-deferred MUST fails closed. ++ ++| Condition | reason_code | ++| --- | --- | ++| Non-deferred MUST with no admissible assessment | `UNFULFILLED_MUST` (identifies `obligation_id`) | ++| Binding `obligation_id` absent from manifest | `UNKNOWN_OBLIGATION_ID` | ++| Binding `node_id` ∉ `implemented_by` | `WRONG_IMPLEMENTING_NODE` | ++| Strong class claimed/needed without trusted collector | `UNTRUSTED_COLLECTOR` | ++| Weak/model class vs runtime obligation | `INSUFFICIENT_PROVENANCE` | ++| Projection cannot materialize | `GRAPH_PROJECTION_FAILED` | ++| Stored graph ≠ recompute from persistable inputs | `GRAPH_DIVERGENCE` | ++ ++### Collector mapping (strong provenance) ++ ++| `collector.id` + `transport` | Derived class | ++| --- | --- | ++| `node-test` \| `npm-test` \| `node:test` + `tool-execution-transport` | `runtime-observed` | ++| `tool-execution` + `tool-execution-transport` | `tool-produced` | ++| `host-adapter` + `execution-transport` | `host-attested` | ++| `worker` / `worker-transport` / absent / unknown | never strong (`model-reported` if worker; else fail for strong claims) | ++ ++Stored `evidence.provenance` is the derived class. Payload digest is content identity, not origin. Payload string `runtime-observed` without a matching allowlisted collector → `UNTRUSTED_COLLECTOR`. No PKI. ++ ++### `graph_id` preimage and replay ++ ++```text ++graph_id = sha256Fingerprint("assurance-graph/v1", { ++ candidate_id, ++ contract_digest, ++ policy_snapshot_id, ++ execution_graph_digest, // executionGraph.graph_id ++ openspec_input_digest, // fingerprint of canonical OpenSpec/Git input ++ nodes, edges // canonical sorted sets ++}) ++``` ++ ++Persist `canonical_inputs` on the projected graph (additive optional on `assurance-graph/v1`). Replay calls `projectAssuranceGraph` with stored assessments, evidence, verification, and those digests — never ephemeral projector `obligation_ids`. Changing any canonical input yields a new `graph_id`; reconcile of the old graph returns `GRAPH_DIVERGENCE`. ++ ++`rejectForbidden`: allow-list `kind`; reject forbidden `kind` / optional in-memory `namespace` (`finding`, `attestation`, `authorization`, `evaluation-attestation`). Never scan `id`. `REQ-add-authorization-header` with `kind: "requirement"` stays valid. ++ ++`verifyCandidate`: if projection fails, return `ok: false` with `GRAPH_PROJECTION_FAILED` (or `GRAPH_DIVERGENCE` on stored mismatch), omit `assurance_graph`, do not emit `PASS` / `PASS WITH WARNINGS`. Projector missing-candidate / cannot-build uses `GRAPH_PROJECTION_FAILED` (today it misuses `GRAPH_DIVERGENCE`). ++ ++## File Changes ++ ++| File | Action | Description | ++| --- | --- | --- | ++| `schemas/kernel/assessment/v1.schema.json` | Create | Closed `assessment/v1`; no `verdict`. | ++| `schemas/kernel/assessment/fixtures/valid/v1-complete.json` | Create | Full binding fixture. | ++| `schemas/kernel/assessment/fixtures/valid/v1-four-roles.json` | Create | Four payloads, one `evidence_id`, distinct roles. | ++| `schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json` | Create | Missing required fields. | ++| `schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json` | Create | `verdict` rejected. | ++| `schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json` | Create | Assessment must not validate as `evidence/v2`. | ++| `schemas/kernel/manifest.json` | Modify | Register family `assessment`. | ++| `schemas/kernel/contract-claims.json` | Modify | Additive claims; do not replace v2 claims. | ++| `schemas/kernel/assurance-graph/v1.schema.json` | Modify | Optional persistable `canonical_inputs`. Do **not** touch `evidence/v2`, `verification/v2`, K1 v1. | ++| `scripts/lib/independent-verifier/collector-provenance.js` | Create | Allowlist mapper. | ++| `scripts/lib/independent-verifier/assessment.js` | Create | `computeAssessmentId`, emit/validate. | ++| `scripts/lib/independent-verifier/obligation-coverage.js` | Create | MUST walk, deferral, reason codes. | ++| `scripts/lib/independent-verifier/index.js` | Modify | Order, fail-closed projection, return `assessments`. | ++| `scripts/lib/independent-verifier/evidence.js` | Modify | Derive class from collector; keep `computeEvidenceId` observation-only. | ++| `scripts/lib/assurance-graph/projector.js` | Modify | Canonical fingerprint; satisfies from assessments; typed `rejectForbidden`; export it. | ++| `scripts/lib/assurance-graph/index.js` | Modify | Replay from persistable outputs. | ++| `scripts/lib/k6b-schema-fixtures.test.js` | Modify | Assessment fixtures, pin freeze, four-role distinctness. | ++| `scripts/lib/independent-verifier/index.test.js` | Modify | Adversarial MUST / collector / facade cases; add collector on existing harness evidence. | ++| `scripts/lib/assurance-graph/index.test.js` | Modify | `graph_id` inputs, replay, substring-id, structured kind. | ++| `scripts/k6b-verifier-assurance-graph-e2e.test.js` | Modify | Persist assessments + canonical_inputs; replay equality; contract/policy churn. | ++| `docs/roadmaps/harness-evolution.md` | Modify | K6b `revise`; K6c `blocked-by-K6b-remediation` during apply. | ++| `docs/architecture/harness-evolution.md` | Modify | Same status flip (no runtime module). | ++ ++Archive of this change (not apply) restores K6b `done` and K6c `next-eligible`. ++ ++## Interfaces / Contracts ++ ++```javascript ++// $id: ospec://schemas/kernel/assessment/v1 ++{ ++ schema_version: 1, kind: "assessment/v1", ++ assessment_id, // sha256Fingerprint("assessment/v1", { schema_version, kind, evidence_id, role, ++ // obligation_id, node_id, candidate_id, policy_snapshot_id }) ++ evidence_id, role, obligation_id, node_id, candidate_id, policy_snapshot_id ++} // additionalProperties: false; no verdict ++ ++verifyCandidate({ ..., rawEvidence: [{ role, bytes, origin, node_id, obligation_id, ++ collector: { id, transport }, provenance /* claim only */ }] }) ++// -> { ok, strategy, evidence, assessments, verification, assurance_graph } ++// unique-sort(verification.evidence_ids) may list one E; assessments.length may be 4 ++ ++projectAssuranceGraph({ canonicalInputs, candidate, executionGraph, evidence, assessments, verification }) ++``` ++ ++Raw `collector` is harness metadata, never copied onto `evidence/v2`. ++ ++## Requirement Allocation ++ ++| MUST scenario | Allocation | ++| --- | --- | ++| MUST without evidence / unknown `obligation_id` / wrong node | `obligation-coverage.js` | ++| Four roles → four assessments; unique-sort still one E | `assessment.js` + `verdict.js` (unchanged unique-sort) | ++| Facade fail-closed without graph | `independent-verifier/index.js` | ++| Collector-derived strong class; payload-only strong fails; model-reported insufficient; stale/foreign/fabricated | `collector-provenance.js` + `evidence.js` | ++| PASS requires strategy **and** MUST coverage | `index.js` after both gates | ++| Evidence with `verdict` rejected | existing `evidence.js` / schema (unchanged) | ++| `rejectForbidden` by kind/namespace; `REQ-add-authorization-header` valid | `projector.js` | ++| Replay from persistable outputs; `satisfies` from assessments | `assurance-graph/index.js` + projector | ++| Canonical inputs in `graph_id`; contract/policy change → divergence | `projector.js` `sha256Fingerprint` preimage | ++| Matching inputs project; forbidden relations | existing projector + typed reject | ++| Assessment fixtures, cross-family, four-role ids, frozen v2/K1 pins | `assessment/v1.schema.json` + `k6b-schema-fixtures.test.js` | ++| Inventory includes assessment without mutating K6b v2 pins | `manifest.json` / `contract-claims.json` | ++| Roadmap K6b revise / K6c blocked | both `harness-evolution.md` files | ++ ++## Testing Strategy ++ ++`testing.tdd_mode` stays `focused`. Each apply task is RED → GREEN → triangulate (adversarial) → REFACTOR. ++ ++| Layer | What to Test | Approach | ++| --- | --- | --- | ++| Unit | MUST walk, deferral, unknown id, wrong node | Graph with extra MUST and no assessment; deferred skip; alien `obligation_id`. | ++| Unit | Four-role assessments | Same bytes/node/provenance, four roles → one `evidence_id`, four `assessment_id`. | ++| Unit | Collector allowlist | Payload `runtime-observed` without collector fails; allowlisted `node-test` passes. | ++| Unit | `rejectForbidden` | `REQ-add-authorization-header` + `kind: requirement` ok; `kind: authorization` fails. | ++| Contract | `assessment/v1` | Valid complete; missing fields; `verdict`; cross-family vs evidence/verification; byte-identical `evidence/v2`, `verification/v2`, K1 pins. | ++| Unit | `graph_id` preimage | Flip contract, policy, execution-graph, or OpenSpec digest → new id; order permutation unchanged. | ++| Integration | Facade | Stub projector failure → `ok: false`, `GRAPH_PROJECTION_FAILED`, no PASS. | ++| E2E | Replay | Persist assessments + `canonical_inputs`; replay byte-identical; C1→C2 / P1→P2 → `GRAPH_DIVERGENCE`. | ++ ++## Migration / Rollout ++ ++No data migration. Additive schema + runtime gates. Existing verifier tests must supply `collector` on raw evidence that claims a strong class. `graph_id` values change (preimage expansion); static schema fixtures stay valid (pattern-only). Rollback reverts this change as a unit; v2 and K1 pins never move. ++ ++## Open Questions ++ ++None. `sdd-propose-001` is resolved as `ospec://schemas/kernel/assessment/v1`. +diff --git a/openspec/changes/k6b-verification-integrity-remediation/proposal.md b/openspec/changes/k6b-verification-integrity-remediation/proposal.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/proposal.md +@@ -0,0 +1,74 @@ ++# Proposal: k6b-verification-integrity-remediation ++ ++## Intent ++ ++Cerrar seis defectos verificados del verifier y Assurance Graph de K6b (v2.50.0) sin reabrir el macro-slice ni iniciar K6c–K8. `evaluateStrategy()` solo comprueba roles: **strategy satisfied ≠ Execution Graph satisfied**. PASS exige evidencia admisible por cada MUST, bound al nodo correcto, con binding persistible y proyección fail-closed. ++ ++## Scope ++ ++### In Scope ++- Walk MUST / Obligation Manifest / `required_evidence`; PASS solo con evidencia admisible en el `node_id` correcto. ++- Binding aditivo persistible (assessment o verification evolucionada); no mutar `evidence/v2` ni K1 v1. ++- Provenance fuerte derivada del collector/transport (digest ≠ origen; sin PKI). ++- `graph_id` fingerprinta contract digest, policy snapshot, execution-graph digest e input OpenSpec; replay persistido. ++- Proyección fallida ⇒ `GRAPH_DIVERGENCE` / `GRAPH_PROJECTION_FAILED`; facade no retorna `ok: true` sin grafo. ++- `rejectForbidden` por kind/namespace, no substring del id. ++- Tests adversariales (MUST sin evidencia, obligation_id inexistente, nodo incorrecto, un EvidenceId en cuatro roles, provenance reclasificada, replay, cambio de contract/policy). ++- Roadmap: K6b `revise`; K6c `blocked-by-K6b-remediation` hasta archive; luego K6b `done`, K6c `next-eligible`. ++ ++### Out of Scope ++- Macro-slice K6b; K6c/K6d/K7/K8; PKI; mutar `evidence/v2` o K1 v1; cambiar el compilador K4a. ++ ++## Capabilities ++ ++### New Capabilities ++- None. ++ ++### Modified Capabilities ++- `independent-verification`: cobertura MUST, assessment persistible, provenance de collector, proyección fail-closed. ++- `assurance-graph`: fingerprint de canonicalInputs, replay persistido, `rejectForbidden` por kind. ++- `kernel-contract-schemas`: familia aditiva de assessment/binding; pins `evidence/v2`, `verification/v2` y K1 v1 intactos. ++ ++## Approach ++ ++Tras strategy, evaluar MUST no diferidas con assessments persistibles (role + obligation + node + policy). `evidence/v2` permanece observación; el assessment id evita colapsar cuatro roles. Mapear collector/transport a clase fuerte; payload sin canal de confianza falla cerrado. Incluir canonicalInputs en `graph_id`. Si `projectAssuranceGraph` falla, el facade falla cerrado. Docs: K6b `revise`, K6c blocked. ++ ++## Affected Areas ++ ++| Area | Impact | Description | ++|------|--------|-------------| ++| `scripts/lib/independent-verifier/` | Modified | Walk MUST, binding, provenance, fail-closed | ++| `scripts/lib/assurance-graph/projector.js` | Modified | canonicalInputs; kind/namespace | ++| `schemas/kernel/` | New (additive) | Assessment/binding + fixtures | ++| `scripts/k6b-verifier-assurance-graph-e2e.test.js` | Modified | Replay persistido | ++| `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 | ++|------|------------|------------| ++| Consumers leen solo `evidence/v2` | High | Schema aditivo + E2E persistido | ++| Unique-sort oculta roles | High | Assessment id incluye role+obligation | ++| Collector mal mapeado | Med | Allowlist fail-closed | ++| 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 ++ ++- K6b archivado (`2026-08-27-k6b-verifier-evidence-assurance-graph`); Obligation Manifest K4a; schemas `evidence/v2`, `verification/v2`, `assurance-graph/v1`, `execution-graph/v1`. Delivery: `exception-ok`. ++ ++## Success Criteria ++ ++- [ ] MUST sin evidencia, `obligation_id` inexistente o nodo incorrecto ⇒ FAIL cerrado. ++- [ ] Cuatro roles sobre la misma observación producen assessments distintos. ++- [ ] Provenance fuerte no se acepta solo desde el payload. ++- [ ] Replay desde salidas persistidas; cambio de contract/policy cambia digest y reconcilia. ++- [ ] Proyección fallida ⇒ `ok: false` (`GRAPH_DIVERGENCE` o `GRAPH_PROJECTION_FAILED`). ++- [ ] `REQ-add-authorization-header` no se rechaza por substring. ++- [ ] Roadmap: K6b `revise` y 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-verification-integrity-remediation/specs/assurance-graph/spec.md b/openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md +@@ -0,0 +1,119 @@ ++# Delta for Assurance Graph ++ ++## ADDED Requirements ++ ++### 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. ++ ++#### 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 ++- 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 ++ ++## MODIFIED Requirements ++ ++### 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: 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` +diff --git a/openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md b/openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md +@@ -0,0 +1,158 @@ ++# Delta for Independent Verification ++ ++## ADDED 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 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 ++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. ++ ++#### 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 ++ ++### 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 ++ ++- 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 ++- 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 ++ ++### 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` ++ ++## MODIFIED Requirements ++ ++### 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: 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 +diff --git a/openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md b/openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md +@@ -0,0 +1,120 @@ ++# Delta for kernel-contract-schemas ++ ++## ADDED 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}$`), 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. ++ ++#### Scenario: Valid assessment fixture passes ++ ++- GIVEN a complete assessment/binding payload with role, obligation_id, node_id, evidence_id, and policy-snapshot identity ++- 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: 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 ++- THEN those schema and fixture bytes and K1 pins MUST remain byte-identical ++ ++## MODIFIED Requirements ++ ++### 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 +diff --git a/openspec/changes/k6b-verification-integrity-remediation/state.yaml b/openspec/changes/k6b-verification-integrity-remediation/state.yaml +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/state.yaml +@@ -0,0 +1,163 @@ ++change: "k6b-verification-integrity-remediation" ++status: "verified" ++last_updated: "2026-08-27T15:30:00Z" ++classification: high-risk ++blocking_questions: [] ++ ++owner: ++ author: Manuel Retamozo ++ branch: feat/k6b-verifier-evidence-assurance-graph ++ ++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-27T13:33:00Z" ++ synthesis: "Remediación focal k6b-verification-integrity-remediation sobre el verifier y el Assurance Graph de K6b, sin reabrir el macro-slice ni tocar K6c/K6d/K7/K8. Un PASS exigirá evidencia admisible para cada obligación MUST del Execution Graph; el binding EvidenceId ↔ role ↔ obligation_id ↔ node_id ↔ policy será persistible y versionado (sin mutar evidence/v2 in-place); la provenance fuerte saldrá del collector/canal, no del payload; la proyección fallará cerrada y su fingerprint ligará los inputs canónicos. El roadmap marcará K6b revise y K6c bloqueado hasta ese cierre." ++ scope: "In: cobertura de obligaciones MUST del Execution Graph; binding persistible EvidenceId↔role↔obligation↔node↔policy (contrato versionado aditivo); provenance fuerte derivada del collector; fingerprint canónico del Assurance Graph; fail-closed si falla la proyección; tests adversariales; actualización de roadmap K6b revise / K6c blocked-by-remediation. Out: K6c challenges, K7 review, K8 attestation, PKI/criptografía de provenance, mutación in-place de evidence/v2 o contratos K1." ++ applies_to: ++ - change-classification ++ - id: execution-mode-001 ++ gate: execution-mode ++ decision: auto ++ source: vscode/askQuestions ++ accepted_at: "2026-08-27T13:39: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-27T13:39:30Z" ++ applies_to: ++ - sdd-tasks ++ - sdd-apply ++ - id: 4r-remediation-001 ++ gate: review-workload ++ decision: remediate-criticals-and-warnings ++ source: vscode/askQuestions ++ accepted_at: "2026-08-27T15:00:00Z" ++ applies_to: ++ - sdd-apply ++ - review-correction ++ - id: k6b-verification-integrity-remediation-bounded-review-001 ++ gate: 4r-review-gate ++ decision: new-candidate ++ source: vscode/askQuestions ++ accepted_at: "2026-08-27T18:45:00Z" ++ reason: "Successor new-candidate: migrate harness collector off worker envelopes, green the verifier suite, and close remaining advisory WARNINGs that fit." ++ applies_to: ++ - sdd-design ++ - sdd-tasks ++ - sdd-apply ++ - sdd-verify ++ ++baseline_fingerprints: ++ independent-verification: "sha256:8063703bb9860b0fbff8b49894fa17932c222cb58f1dea028bda57a00330fc6d" ++ assurance-graph: "sha256:c7d171aab463f80f504aff6706e7847ef44a849ae79a8e06f13a8e4e17a1795d" ++ kernel-contract-schemas: "sha256:fabcbc4edb0ab2934414ca9bf5bca0f88c7e5a3759a0c427f739d0475835d25d" ++ ++assumptions: ++ - id: sdd-propose-001 ++ phase: sdd-propose ++ statement: "The persistable binding is an additive assessment/binding family; the exact schema $id is left to sdd-design." ++ reversibility: high ++ basis: "Accepted intent-briefing forbids in-place evidence/v2 mutation and allows an additive assessment structure or verification-contract evolution; pinning $id in the proposal would pre-empt design." ++ recorded_at: "2026-08-27T13:43:36Z" ++ status: resolved ++ resolved_by: sdd-design ++ resolved_at: "2026-08-27T14:05:00Z" ++ resolution: "ospec://schemas/kernel/assessment/v1 (kind assessment/v1; manifest key assessment; ADR-001)" ++ ++phases: ++ proposal: ++ status: done ++ artifact: "openspec/changes/k6b-verification-integrity-remediation/proposal.md" ++ summary: "Cierra 6 defectos K6b: cobertura MUST, binding persistible, provenance de collector, fingerprint canónico, proyección fail-closed y rejectForbidden por kind." ++ key_decisions: ++ - "Familia aditiva de assessment/binding; no mutar evidence/v2 ni K1 v1" ++ - "PASS exige evidencia admisible por cada MUST bound al nodo correcto" ++ - "Roadmap: K6b revise; K6c blocked-by-K6b-remediation hasta archive" ++ spec: ++ status: done ++ artifacts: ++ - "openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md" ++ - "openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md" ++ - "openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md" ++ summary: "Deltas: MUST coverage, persistable assessments, collector provenance, canonical graph_id, fail-closed projection, kind/namespace rejectForbidden." ++ key_decisions: ++ - "PASS requires MUST obligation coverage bound to implementing nodes; strategy shape is insufficient" ++ - "Additive assessment identity includes role+obligation_id; evidence/v2 unchanged" ++ - "graph_id fingerprints canonicalInputs; rejectForbidden by kind/namespace; facade fail-closed" ++ clarify: ++ status: skipped ++ design: ++ status: done ++ artifact: "openspec/changes/k6b-verification-integrity-remediation/design.md" ++ summary: "Additive assessment/v1; MUST walk after strategy; collector allowlist; canonical graph_id; facade fail-closed; kind/namespace rejectForbidden." ++ key_decisions: ++ - "Schema $id ospec://schemas/kernel/assessment/v1; evidence/v2 unchanged (ADR-001)" ++ - "evaluateStrategy then MUST walk; assessment_id includes role+obligation_id (ADR-002)" ++ - "graph_id fingerprints canonicalInputs; projection failure is GRAPH_PROJECTION_FAILED (ADR-004)" ++ tasks: ++ status: done ++ artifact: "openspec/changes/k6b-verification-integrity-remediation/tasks.md" ++ summary: "8 fases, 28 tareas: assessment/v1 schema, MUST walk, collector provenance, facade fail-closed, graph_id canónico, replay, adversariales, roadmap." ++ key_decisions: ++ - "size-exception aprobado (~900–1200 líneas); single PR lógico en 4 work units" ++ - "TDD RED→GREEN→TRIANGULATE por módulo; adversariales como tareas de primera clase" ++ - "Publicación assessment/v1 en Phase 1 (lección K6b V001)" ++ apply: ++ status: done ++ artifact: "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md" ++ summary: "Phases 1–8 done: assessment/v1, MUST walk, collector allowlist, facade fail-closed, canonical graph_id, E2E replay, K6b revise." ++ key_decisions: ++ - "size:exception single batch; v2/K1 pins unchanged" ++ - "satisfies edges rebuild from persistable assessments only" ++ - "payload-only strong provenance is UNTRUSTED_COLLECTOR" ++ verify: ++ status: done ++ artifact: "openspec/changes/k6b-verification-integrity-remediation/verify-report.md" ++ summary: "PASS: 30/30 tasks; 35/35 MUST scenarios with runtime-test or static-proof; npm test 2744 pass, 0 fail." ++ key_decisions: ++ - "Lineage absent → full discovery; sdd-propose-001 already resolved" ++ - "quality_gates policy absent; no known-issues write (no CRITICAL/WARNING)" ++ archive: ++ status: pending ++ artifact: "openspec/changes/k6b-verification-integrity-remediation/archive-report.md" ++ ++gates: ++ 4r-review-gate: ++ status: correction-required ++ schema_version: 1 ++ classification: high-risk ++ depth: ++ review: strict ++ lineage_status: validating ++ lineage_id: "sha256:262dda4ab0b3ec0fe60b7db34683c55d3c4d2590fe69282c40197cbc95aacf42" ++ lineage_revision: 11 ++ lineage_artifact: openspec/changes/k6b-verification-integrity-remediation/.4r/lineage.json ++ findings_summary: "0 BLOCKER, 4 CRITICAL, 9 WARNING, 1 SUGGESTION" ++ archive_allowed: false ++ selected_specialists: [risk, reliability, resilience, readability] ++ evidence: ++ schema_version: 1 ++ fingerprint: "sha256:66b1b1c10af6adfd036aa014032450b530160d81f05f9dae3af5c1cf09ade779" ++ generalist: ++ status: needs-specialist ++ specialists: [risk, reliability] ++ reason: "signals=design-risk,diff-auth-permission,metadata-runtime;dimensions=risk,reliability" +diff --git a/openspec/changes/k6b-verification-integrity-remediation/tasks.md b/openspec/changes/k6b-verification-integrity-remediation/tasks.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/tasks.md +@@ -0,0 +1,122 @@ ++# Tasks: k6b-verification-integrity-remediation ++ ++## Spec/Design Reconciliation ++ ++| Requirement / Scenario | Priority | Design Allocation | Status | Notes | ++|------------------------|----------|-------------------|--------|-------| ++| REQ-independent-verification-005: MUST without evidence fails closed | MUST | `obligation-coverage.js` | covered-by-design | `UNFULFILLED_MUST` after strategy | ++| REQ-independent-verification-005: Nonexistent obligation_id fails closed | MUST | `obligation-coverage.js` | covered-by-design | `UNKNOWN_OBLIGATION_ID` | ++| REQ-independent-verification-005: Wrong implementing node fails closed | MUST | `obligation-coverage.js` | covered-by-design | `WRONG_IMPLEMENTING_NODE` | ++| REQ-independent-verification-006: Four roles → four assessments | MUST | `assessment.js` + `verdict.js` | covered-by-design | Assessment id includes role+obligation | ++| REQ-independent-verification-007: Failed projection → ok:false | MUST | `independent-verifier/index.js` | covered-by-design | No PASS without graph | ++| REQ-independent-verification-003: Collector-derived runtime-observed | MUST | `collector-provenance.js`, `evidence.js` | covered-by-design | Allowlist fail-closed | ++| REQ-independent-verification-003: Model-reported insufficient | MUST | `collector-provenance.js`, `evidence.js` | covered-by-design | `INSUFFICIENT_PROVENANCE` | ++| REQ-independent-verification-003: Stale/foreign/fabricated rejected | MUST | `evidence.js` (existing) | covered-by-design | Unchanged gates | ++| REQ-independent-verification-003: Payload-only strong fails closed | MUST | `collector-provenance.js` | covered-by-design | `UNTRUSTED_COLLECTOR` | ++| REQ-independent-verification-004: PASS requires strategy + MUST coverage | MUST | `index.js`, `verdict.js` | covered-by-design | Both gates required | ++| REQ-independent-verification-004: Evidence carrying verdict rejected | MUST | `evidence.js`, schemas (unchanged) | covered-by-design | Existing v2 guard | ++| REQ-assurance-graph-005: REQ-add-authorization-header valid | MUST | `projector.js` `rejectForbidden` | covered-by-design | Kind/namespace only | ++| REQ-assurance-graph-005: Structured authorization kind rejected | MUST | `projector.js` | covered-by-design | Forbidden kind/namespace | ++| REQ-assurance-graph-006: Replay byte-identical from persistable outputs | MUST | `assurance-graph/index.js` | covered-by-design | No ephemeral obligation_ids | ++| REQ-assurance-graph-001: Matching inputs project graph | MUST | `projector.js` | covered-by-design | canonicalInputs in preimage | ++| REQ-assurance-graph-001: Divergent graph fails closed | MUST | `assurance-graph/index.js` | covered-by-design | `GRAPH_DIVERGENCE` | ++| REQ-assurance-graph-001: Contract/policy change → divergence | MUST | `projector.js`, reconcile | covered-by-design | C1→C2 / P1→P2 | ++| REQ-assurance-graph-002: Same inputs same digest | MUST | `projector.js` | covered-by-design | Sorted canonical sets | ++| REQ-assurance-graph-002: Forbidden relations rejected | MUST | `projector.js`, schema | covered-by-design | Existing + typed reject | ++| REQ-assurance-graph-002: Canonical input change → distinct graph_id | MUST | `projector.js` | covered-by-design | contract/policy/exec/openspec digests | ++| REQ-kernel-contract-schemas-027: Valid assessment fixture passes | MUST | `assessment/v1.schema.json`, fixtures | covered-by-design | ADR-001 $id | ++| REQ-kernel-contract-schemas-027: Cross-family + verdict fail closed | MUST | fixtures + `k6b-schema-fixtures.test.js` | covered-by-design | No evidence/verification alias | ++| REQ-kernel-contract-schemas-027: Four-role distinct identities | MUST | `assessment.js`, fixtures | covered-by-design | `v1-four-roles.json` | ++| REQ-kernel-contract-schemas-027: v2/K1 pins frozen | MUST | `k6b-schema-fixtures.test.js` | covered-by-design | Byte-identical assert | ++| REQ-kernel-contract-schemas-001: Assessment in inventory | MUST | `manifest.json`, `contract-claims.json` | covered-by-design | Additive registration | ++ ++### Reconciliation Verdict ++ ++- MUST coverage: complete (12 REQs, 24/24 MUST scenarios covered-by-design). ++- SHOULD/MAY gaps: none. ++- Ambiguities to track: none. ++ ++## Review Workload Forecast ++ ++| Field | Value | ++|-------|-------| ++| Estimated changed lines | ~900–1200 líneas | ++| 400-line budget risk | High | ++| Chained PRs recommended | Yes | ++| Suggested split | Single PR con `size:exception`; orden: assessment/v1 schema → verifier modules → assurance-graph → adversarial/E2E → roadmap docs | ++| 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 | Publicar `assessment/v1` schema, fixtures y registro aditivo | PR 1 (single) | manifest + contract-claims; v2/K1 pins byte-identical | ++| 2 | Módulos verifier: assessment, collector-provenance, obligation-coverage, facade | PR 1 (single) | Strategy → MUST walk → project fail-closed | ++| 3 | Assurance Graph: canonical graph_id, rejectForbidden, replay | PR 1 (single) | satisfies desde assessments persistibles | ++| 4 | Tests adversariales, E2E replay y roadmap K6b revise / K6c blocked | PR 1 (single) | `k6b-verifier-assurance-graph-e2e.test.js` + harness-evolution docs | ++ ++### Checklist Status Legend ++ ++- `[ ]` Not implemented yet ++- `[~]` Implemented but not yet verified locally ++- `[x]` Implemented and verified locally ++ ++## Phase 1: Assessment/v1 Schema Publication ++ ++- [x] 1.1 RED: Extender `scripts/lib/k6b-schema-fixtures.test.js` con tests que exigen familia `assessment` en `manifest.json`/`contract-claims.json`, fixtures valid/invalid, cross-family rejection y assert byte-identical de `evidence/v2`, `verification/v2` y pins K1. [REQ-kernel-contract-schemas-027, REQ-kernel-contract-schemas-001] ++- [x] 1.2 GREEN: Crear `schemas/kernel/assessment/v1.schema.json` (`$id: ospec://schemas/kernel/assessment/v1`, `kind: assessment/v1`, `additionalProperties: false`, sin `verdict`). [REQ-kernel-contract-schemas-027] ++- [x] 1.3 GREEN: Crear fixtures `schemas/kernel/assessment/fixtures/valid/v1-complete.json`, `v1-four-roles.json` e invalid `v1-missing-required.json`, `v1-with-verdict.json`, `v1-evidence-alias.json`. [REQ-kernel-contract-schemas-027] ++- [x] 1.4 GREEN: Registrar familia `assessment` en `schemas/kernel/manifest.json` y claims aditivos en `schemas/kernel/contract-claims.json`; ejecutar `node --test scripts/lib/k6b-schema-fixtures.test.js`. [REQ-kernel-contract-schemas-001] ++- [x] 1.5 TRIANGULATE: Assert cuatro roles comparten un `evidence_id` pero producen cuatro `assessment_id` distintos en fixtures + test. [REQ-kernel-contract-schemas-027] ++ ++## Phase 2: Assessment Identity Module ++ ++- [x] 2.1 RED: Crear `scripts/lib/independent-verifier/assessment.test.js` con tests para `computeAssessmentId`, emisión/validación y rechazo de payloads con `verdict`. [REQ-independent-verification-006] ++- [x] 2.2 GREEN: Implementar `scripts/lib/independent-verifier/assessment.js` (`computeAssessmentId`, `emitAssessment`, validate contra schema). [REQ-independent-verification-006] ++- [x] 2.3 TRIANGULATE: Mismo bytes/provenance/node, cuatro roles → un `evidence_id`, cuatro `assessment_id` distintos. [REQ-independent-verification-006] ++ ++## Phase 3: Collector Provenance Allowlist ++ ++- [x] 3.1 RED: Tests en `scripts/lib/independent-verifier/index.test.js` para payload `runtime-observed` sin collector → `UNTRUSTED_COLLECTOR`; allowlisted `node-test`+`tool-execution-transport` → `runtime-observed`; worker → `model-reported` insuficiente para obligación runtime. [REQ-independent-verification-003] ++- [x] 3.2 GREEN: Crear `scripts/lib/independent-verifier/collector-provenance.js` con tabla allowlist fail-closed (ADR-003). [REQ-independent-verification-003] ++- [x] 3.3 GREEN: Modificar `scripts/lib/independent-verifier/evidence.js` para derivar clase desde collector/transport; mantener `computeEvidenceId` observation-only. [REQ-independent-verification-003] ++- [x] 3.4 TRIANGULATE: Provenance reclasificada (payload strong vs collector weak) falla cerrado; digest ≠ origen. [REQ-independent-verification-003] ++ ++## Phase 4: Obligation Manifest MUST Walk ++ ++- [x] 4.1 RED: Tests en `scripts/lib/independent-verifier/obligation-coverage.test.js` (o `index.test.js`): MUST sin evidencia → `UNFULFILLED_MUST`; `obligation_id` alien → `UNKNOWN_OBLIGATION_ID`; nodo B ∉ `implemented_by` → `WRONG_IMPLEMENTING_NODE`; deferral aprobado skip. [REQ-independent-verification-005] ++- [x] 4.2 GREEN: Implementar `scripts/lib/independent-verifier/obligation-coverage.js` con walk post-strategy, deferral predicate K4a y emisión de assessments persistibles. [REQ-independent-verification-005] ++- [x] 4.3 TRIANGULATE: Strategy satisfecha pero MUST sin binding → FAIL; empty `required_evidence` en MUST no-deferred → FAIL. [REQ-independent-verification-005] ++ ++## Phase 5: Verifier Facade Integration ++ ++- [x] 5.1 RED: Tests en `index.test.js`: orden bindings→strategy→normalize→MUST→project; stub projector failure → `ok: false`, `GRAPH_PROJECTION_FAILED`, sin `assurance_graph` ni PASS; strategy fail short-circuits sin upgrade MUST. [REQ-independent-verification-007, REQ-independent-verification-004] ++- [x] 5.2 GREEN: Modificar `scripts/lib/independent-verifier/index.js`: integrar MUST walk, retornar `assessments`, fail-closed si proyección falla; PASS solo con strategy + MUST coverage. [REQ-independent-verification-004, REQ-independent-verification-007] ++- [x] 5.3 GREEN: Actualizar tests existentes en `index.test.js` para incluir `collector` en raw evidence que reclama clase fuerte. [REQ-independent-verification-003] ++- [x] 5.4 REFACTOR: Verificar `verification.evidence_ids` unique-sort sigue listando un E con cuatro assessments distintos. [REQ-independent-verification-006, REQ-independent-verification-004] ++ ++## Phase 6: Assurance Graph Remediation ++ ++- [x] 6.1 RED: Tests en `scripts/lib/assurance-graph/index.test.js`: `graph_id` cambia al alterar contract/policy/execution-graph/openspec digest; permutación nodos/edges no cambia digest; `REQ-add-authorization-header` + `kind: requirement` válido; `kind: authorization` rechazado. [REQ-assurance-graph-002, REQ-assurance-graph-005] ++- [x] 6.2 GREEN: Modificar `scripts/lib/assurance-graph/projector.js`: preimage canonicalInputs en `graph_id`; `satisfies` desde assessments; exportar `rejectForbidden` por kind/namespace; `GRAPH_PROJECTION_FAILED` para missing-candidate. [REQ-assurance-graph-002, REQ-assurance-graph-005] ++- [x] 6.3 GREEN: Modificar `schemas/kernel/assurance-graph/v1.schema.json` con campo opcional `canonical_inputs` persistible. [REQ-assurance-graph-002] ++- [x] 6.4 RED: Tests replay: recomputar desde assessments+evidence+verification+canonical_inputs sin ephemeral `obligation_ids` → byte-identical; C1→C2 / P1→P2 → `GRAPH_DIVERGENCE`. [REQ-assurance-graph-006, REQ-assurance-graph-001] ++- [x] 6.5 GREEN: Modificar `scripts/lib/assurance-graph/index.js` para replay desde salidas persistibles y reconciliación fail-closed. [REQ-assurance-graph-006] ++ ++## Phase 7: Adversarial Tests and E2E ++ ++- [x] 7.1 RED: Extender `scripts/k6b-verifier-assurance-graph-e2e.test.js` con persistencia de assessments + `canonical_inputs`, replay equality y contract/policy churn → `GRAPH_DIVERGENCE`. [REQ-assurance-graph-006, REQ-assurance-graph-001] ++- [x] 7.2 GREEN: Implementar flujo E2E completo con assessments persistidos y replay byte-identical. [REQ-assurance-graph-006] ++- [x] 7.3 TRIANGULATE adversariales en `index.test.js`: un EvidenceId en cuatro roles; MUST sin evidencia; obligation_id inexistente; nodo incorrecto; proyección fallida. [REQ-independent-verification-005, REQ-independent-verification-006, REQ-independent-verification-007] ++- [x] 7.4 Ejecutar suites focalizadas (`k6b-schema-fixtures.test.js`, `independent-verifier/*.test.js`, `assurance-graph/index.test.js`, `k6b-verifier-assurance-graph-e2e.test.js`) y registrar evidencia TDD en `apply-progress.md`. ++ ++## Phase 8: Roadmap Documentation ++ ++- [x] 8.1 Actualizar `docs/roadmaps/harness-evolution.md`: K6b `revise`; K6c `blocked-by-K6b-remediation` hasta archive de este change. [REQ-independent-verification-005 via proposal success criteria] ++- [x] 8.2 Actualizar `docs/architecture/harness-evolution.md` con el mismo flip de estado (sin módulos runtime). [REQ-independent-verification-005 via proposal success criteria] +diff --git a/openspec/changes/k6b-verification-integrity-remediation/verify-report.md b/openspec/changes/k6b-verification-integrity-remediation/verify-report.md +new file mode 100644 +--- /dev/null ++++ b/openspec/changes/k6b-verification-integrity-remediation/verify-report.md +@@ -0,0 +1,142 @@ ++## Verification Report ++ ++**Change**: k6b-verification-integrity-remediation ++**Version**: N/A (delta over archived K6b v2.50.0) ++**Mode**: Standard (focused TDD; Strict TDD inactive) ++ ++### Completeness ++| Metric | Value | ++|--------|-------| ++| Tasks total | 30 | ++| Tasks complete | 30 | ++| Tasks incomplete | 0 | ++ ++All Phase 1–8 items in `tasks.md` are `[x]`. `apply-progress.md` records the same batch as complete (size:exception, single batch). ++ ++Lineage router: `verify_lineage` absent → `run-discovery`. Assumption `sdd-propose-001` is already `resolved` (design `$id`); Step 2b was a no-op. `quality_gates:` is commented out in `openspec/config.yaml` → Step 9a skipped. ++ ++### Build & Tests Execution ++**Build**: ✅ Passed (no dedicated `build_command`; `npm test` → `node scripts/check.js` generated and validated targets; ended with `All checks passed.`) ++```text ++npm test ++==> Native Node tests ++ℹ tests 2746 ++ℹ pass 2744 ++ℹ fail 0 ++ℹ skipped 2 ++ℹ duration_ms 62890.3026 ++==> Generate + validate (claude generation-only; other targets validated) ++All checks passed. ++exit 0 ++``` ++ ++**Tests**: ✅ 2744 passed / ❌ 0 failed / ⚠️ 2 skipped ++```text ++Command: npm test (node scripts/check.js) ++Native runner: node --test scripts/**/*.test.js ++Skipped tests are unrelated platform/git/symlink probes, not this change. ++Focal suites exercised in-process: k6b-schema-fixtures, independent-verifier/{assessment,obligation-coverage,index}, assurance-graph/index, k6b-verifier-assurance-graph-e2e, k2a-maturity-docs, k1-scope-guard, k1-schema-compat. ++``` ++ ++**Manual verification**: not performed ++```text ++N/A — automated runtime evidence covers all MUST scenarios. ++``` ++ ++**Coverage**: ➖ Not available / threshold: 0% → ➖ Not available (`testing.coverage.available: false`) ++ ++### Spec Compliance Matrix ++| Requirement | Scenario | Evidence Level | Source | Result | Notes | ++|-------------|----------|----------------|--------|--------|-------| ++| REQ-independent-verification-005 | MUST without admissible evidence fails closed | `runtime-test` | `obligation-coverage.test.js` > UNFULFILLED_MUST; `index.test.js` after strategy | PASS | Identifies `req-repair-001`; no verification emitted | ++| REQ-independent-verification-005 | Nonexistent obligation_id fails closed | `runtime-test` | `obligation-coverage.test.js`; `index.test.js` alien id | PASS | `UNKNOWN_OBLIGATION_ID` | ++| REQ-independent-verification-005 | Evidence bound to the wrong implementing node fails closed | `runtime-test` | `obligation-coverage.test.js`; `index.test.js` other-node | PASS | `WRONG_IMPLEMENTING_NODE` | ++| REQ-independent-verification-006 | Same EvidenceId used as four roles yields four assessments | `runtime-test` | `assessment.test.js`; `index.test.js` four-role facade | PASS | unique-sort `evidence_ids` size 1; four `assessment_id` | ++| REQ-independent-verification-007 | Failed projection does not return ok without a graph | `runtime-test` | `index.test.js` projector stub | PASS | `GRAPH_PROJECTION_FAILED`; no `assurance_graph`; no verification | ++| REQ-independent-verification-003 | Runtime-observed evidence satisfies a test obligation | `runtime-test` | `index.test.js` allowlisted node-test collector | PASS | Stored provenance derived; collector not copied onto evidence/v2 | ++| REQ-independent-verification-003 | Model-reported tests-passed is insufficient | `runtime-test` | `index.test.js` worker collector | PASS | `INSUFFICIENT_PROVENANCE` / `UNTRUSTED_COLLECTOR` | ++| REQ-independent-verification-003 | Stale, foreign, or fabricated evidence is rejected | `runtime-test` | `index.test.js` FOREIGN_SUBJECT / FABRICATED_EVIDENCE / STALE_EVIDENCE | PASS | Unchanged gates retained | ++| REQ-independent-verification-003 | Payload-claimed strong provenance without trusted collector fails closed | `runtime-test` | `index.test.js` UNTRUSTED_COLLECTOR; payload vs worker | PASS | Digest is not origin | ++| REQ-independent-verification-004 | Sufficient evidence yields a verification verdict | `runtime-test` | `index.test.js` PASS and PASS WITH WARNINGS | PASS | Requires strategy + MUST walk + projection | ++| REQ-independent-verification-004 | Evidence carrying verdict is rejected | `runtime-test` | `index.test.js` MIXED_EVIDENCE_VERDICT | PASS | | ++| REQ-assurance-graph-005 | Requirement id containing authorization remains valid | `runtime-test` | `assurance-graph/index.test.js` rejectForbidden | PASS | `REQ-add-authorization-header` + `kind: requirement` | ++| REQ-assurance-graph-005 | Structured authorization kind is rejected | `runtime-test` | `assurance-graph/index.test.js` kind + namespace | PASS | `FORBIDDEN_RELATION` | ++| REQ-assurance-graph-006 | Replay from persisted outputs yields the same graph | `runtime-test` | `assurance-graph/index.test.js`; `k6b-verifier-assurance-graph-e2e.test.js` | PASS | Assessments + `canonical_inputs`; no ephemeral obligation_ids | ++| REQ-assurance-graph-001 | Matching canonical inputs project a graph | `runtime-test` | `assurance-graph/index.test.js`; e2e double project | PASS | | ++| REQ-assurance-graph-001 | Divergent graph fails closed | `runtime-test` | `assurance-graph/index.test.js` reconcile mutated graph_id/edges | PASS | `GRAPH_DIVERGENCE` | ++| REQ-assurance-graph-001 | Contract or policy change forces reconciliation fail-closed | `runtime-test` | `assurance-graph/index.test.js`; e2e C1→C2 | PASS | Distinct `graph_id` then `GRAPH_DIVERGENCE` | ++| REQ-assurance-graph-002 | Same inputs yield the same digest and edges | `runtime-test` | `assurance-graph/index.test.js` permutation; e2e | PASS | Sorted canonical sets | ++| REQ-assurance-graph-002 | Forbidden later-slice relations are rejected | `runtime-test` | `k6b-schema-fixtures.test.js` reviewed-by; `rejectForbidden` | PASS | | ++| REQ-assurance-graph-002 | Canonical input change yields a distinct graph_id | `runtime-test` | `assurance-graph/index.test.js` contract/policy/exec/openspec flips | PASS | Preimage includes all four digests + candidate + nodes/edges | ++| REQ-kernel-contract-schemas-027 | Valid assessment fixture passes | `runtime-test` | `k6b-schema-fixtures.test.js` v1-complete.json | PASS | `$id` ospec://schemas/kernel/assessment/v1 | ++| REQ-kernel-contract-schemas-027 | Cross-family substitution and verdict fail closed | `runtime-test` | `k6b-schema-fixtures.test.js` verdict + alias | PASS | additionalProperties: false | ++| REQ-kernel-contract-schemas-027 | Four-role assessments remain distinct under the schema | `runtime-test` | `k6b-schema-fixtures.test.js` v1-four-roles.json | PASS | Array of four payloads; one evidence_id | ++| REQ-kernel-contract-schemas-027 | Evidence v2, verification v2, and K1 v1 pins remain frozen | `static-proof` | `k6b-schema-fixtures.test.js` digest pins + `assertK1SchemasUnchanged` | PASS | Byte-identical assert (accepted for structural MUST) | ++| REQ-kernel-contract-schemas-001 | Every required family has $id and version | `runtime-test` | `kernel-schema-fixtures.test.js` + later kN fixture suites + assessment registration | PASS | Assessment added to manifest | ++| REQ-kernel-contract-schemas-001 | Consumer can pin a schema version | `runtime-test` | `loadSchemaById("ospec://schemas/kernel/assessment/v1")` | PASS | | ++| REQ-kernel-contract-schemas-001 | K2.1 families are included | `runtime-test` | existing K2.1 schema fixture suite (npm test) | PASS | Unchanged by this delta | ++| REQ-kernel-contract-schemas-001 | K2a families are included | `runtime-test` | existing K2a schema fixture suite (npm test) | PASS | Unchanged by this delta | ++| REQ-kernel-contract-schemas-001 | k2a-1 transport envelope families are included | `runtime-test` | existing transport envelope fixture suite (npm test) | PASS | Unchanged by this delta | ++| REQ-kernel-contract-schemas-001 | K3 execution identity families are included | `runtime-test` | `k3-schema-fixtures.test.js` (npm test) | PASS | Unchanged by this delta | ++| REQ-kernel-contract-schemas-001 | K4a families are included | `runtime-test` | `k4a-schema-fixtures.test.js` (npm test) | PASS | Unchanged by this delta | ++| REQ-kernel-contract-schemas-001 | K5 families are included | `runtime-test` | `k5-schema-fixtures.test.js` (npm test) | PASS | Unchanged by this delta | ++| REQ-kernel-contract-schemas-001 | K6a families are included | `runtime-test` | existing K6a schema fixture suite (npm test) | PASS | Unchanged by this delta | ++| REQ-kernel-contract-schemas-001 | K6b assurance-graph family is included | `runtime-test` | `k6b-schema-fixtures.test.js` | PASS | | ++| REQ-kernel-contract-schemas-001 | Assessment/binding family is included without mutating K6b pins | `runtime-test` | `k6b-schema-fixtures.test.js` manifest + v2 digest pins | PASS | Additive registration | ++ ++**Compliance summary**: 35/35 scenarios satisfied at acceptable evidence levels ++ ++### Correctness (Static Evidence) ++| Requirement | Status | Notes | ++|------------|--------|-------| ++| MUST walk after strategy | ✅ Implemented | `verifyCandidate` → evaluateStrategy then `walkMustObligations`; strategy failure short-circuits | ++| Persistable assessment/v1 | ✅ Implemented | `assessment.js` fingerprints role+obligation; evidence/v2 has no role/obligation_id | ++| Collector-derived provenance | ✅ Implemented | `collector-provenance.js` allowlist; payload-only strong → `UNTRUSTED_COLLECTOR` | ++| Facade fail-closed projection | ✅ Implemented | `mapProjectionFailure`; no PASS / no graph on projector failure | ++| Canonical `graph_id` | ✅ Implemented | Preimage: candidate_id, contract_digest, policy_snapshot_id, execution_graph_digest, openspec_input_digest, nodes, edges | ++| `satisfies` from assessments | ✅ Implemented | Projector builds satisfies from persistable assessments only | ++| `rejectForbidden` by kind/namespace | ✅ Implemented | Does not scan `id` substrings | ++| Roadmap K6b revise / K6c blocked | ✅ Implemented | Both harness-evolution docs; `k2a-maturity-docs.test.js` | ++ ++### Coherence (Design) ++| Decision | Followed? | Notes | ++|----------|-----------|-------| ++| ADR-001 `ospec://schemas/kernel/assessment/v1` | ✅ Yes | Manifest key `assessment`; evidence/v2 and verification/v2 untouched | ++| ADR-002 strategy then MUST; assessment id includes role+obligation | ✅ Yes | | ++| ADR-003 collector/transport allowlist, no PKI | ✅ Yes | node-test/npm-test/node:test, tool-execution, host-adapter | ++| ADR-004 canonicalInputs in graph_id; missing candidate is GRAPH_PROJECTION_FAILED | ✅ Yes | | ++| Companion K1 inventory carve-out for `schemas/kernel/assessment/` | ✅ Yes | Documented in apply-progress; assessment is additive, not a K1 family | ++| `v1-four-roles.json` as array of four payloads | ✅ Yes | k1-schema-compat skips via `fixtureNameFilter` (same pattern as `k3-frozen.json`) | ++ ++### Issues Found ++**CRITICAL**: None ++**WARNING**: None ++**SUGGESTION**: ++- `verifyCandidate` maps `GRAPH_DIVERGENCE` if the projector returns it, but the live projector never emits that code. Stored-graph divergence is enforced by `reconcileAssuranceGraph` / replay APIs (covered by REQ-assurance-graph-001/006). Origin: none (non-blocking). ++- `rejectForbidden` lists `FORBIDDEN_KINDS` that cannot match after the `ALLOWED_NODE_KINDS` check; structured rejection still fail-closes via the allow-list. Namespace checks remain live. ++ ++### Traceability Matrix ++| REQ | Tasks | Commits | Tests | Status | ++|-----|-------|---------|-------|--------| ++| REQ-independent-verification-005 | 4.1–4.3, 7.3 | (uncommitted working tree) | `obligation-coverage.test.js`, `index.test.js` | OK | ++| REQ-independent-verification-006 | 2.1–2.3, 5.4, 7.3 | (uncommitted working tree) | `assessment.test.js`, `index.test.js` | OK | ++| REQ-independent-verification-007 | 5.1–5.2, 7.3 | (uncommitted working tree) | `index.test.js` projector stub | OK | ++| REQ-independent-verification-003 | 3.1–3.4, 5.3 | (uncommitted working tree) | `index.test.js` collector/provenance | OK | ++| REQ-independent-verification-004 | 5.1–5.4 | (uncommitted working tree) | `index.test.js` PASS / MIXED_EVIDENCE_VERDICT / short-circuit | OK | ++| REQ-assurance-graph-005 | 6.1–6.2 | (uncommitted working tree) | `assurance-graph/index.test.js` rejectForbidden | OK | ++| REQ-assurance-graph-006 | 6.4–6.5, 7.1–7.2 | (uncommitted working tree) | `assurance-graph/index.test.js`, e2e replay | OK | ++| REQ-assurance-graph-001 | 6.4, 7.1 | (uncommitted working tree) | reconcile + e2e C1→C2 | OK | ++| REQ-assurance-graph-002 | 6.1–6.3 | (uncommitted working tree) | graph_id preimage + permutation | OK | ++| REQ-kernel-contract-schemas-027 | 1.1–1.5 | (uncommitted working tree) | `k6b-schema-fixtures.test.js` | OK | ++| REQ-kernel-contract-schemas-001 | 1.1, 1.4 | (uncommitted working tree) | manifest registration + inherited kN fixture suites | OK | ++ ++No Conventional Commits with `Ospec-Change` / `Ospec-Task` trailers exist yet for this change; implementation is still in the working tree. ++ ++### Assumption Reconciliation ++| id | statement | reversibility | outcome | ++|----|-----------|----------------|---------| ++| sdd-propose-001 | The persistable binding is an additive assessment/binding family; the exact schema $id is left to sdd-design. | high | resolved (sdd-design: `ospec://schemas/kernel/assessment/v1`; no escalation) | ++ ++### Verdict ++PASS ++All 30 tasks complete; 35/35 MUST scenarios have runtime-test or accepted static-proof; `npm test` exited 0 (2744 pass, 0 fail). +diff --git a/schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json b/schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json +new file mode 100644 +--- /dev/null ++++ b/schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json +@@ -0,0 +1,10 @@ ++{ ++ "schema_version": 2, ++ "kind": "evidence/v2", ++ "evidence_id": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ++ "candidate_id": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", ++ "provenance": "runtime-observed", ++ "origin": "npm test", ++ "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", ++ "node_id": "repair-core" ++} +diff --git a/schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json b/schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json +new file mode 100644 +--- /dev/null ++++ b/schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json +@@ -0,0 +1,5 @@ ++{ ++ "schema_version": 1, ++ "kind": "assessment/v1", ++ "assessment_id": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ++} +diff --git a/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json b/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json +new file mode 100644 +--- /dev/null ++++ b/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json +@@ -0,0 +1,12 @@ ++{ ++ "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", ++ "verdict": "PASS" ++} +diff --git a/schemas/kernel/assessment/fixtures/valid/v1-complete.json b/schemas/kernel/assessment/fixtures/valid/v1-complete.json +new file mode 100644 +--- /dev/null ++++ b/schemas/kernel/assessment/fixtures/valid/v1-complete.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/valid/v1-four-roles.json b/schemas/kernel/assessment/fixtures/valid/v1-four-roles.json +new file mode 100644 +--- /dev/null ++++ b/schemas/kernel/assessment/fixtures/valid/v1-four-roles.json +@@ -0,0 +1,46 @@ ++[ ++ { ++ "schema_version": 1, ++ "kind": "assessment/v1", ++ "assessment_id": "sha256:1111111111111111111111111111111111111111111111111111111111111111", ++ "evidence_id": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", ++ "role": "acceptance", ++ "obligation_id": "req-repair-001", ++ "node_id": "repair-core", ++ "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", ++ "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ++ }, ++ { ++ "schema_version": 1, ++ "kind": "assessment/v1", ++ "assessment_id": "sha256:2222222222222222222222222222222222222222222222222222222222222222", ++ "evidence_id": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", ++ "role": "invariants", ++ "obligation_id": "req-repair-001", ++ "node_id": "repair-core", ++ "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", ++ "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ++ }, ++ { ++ "schema_version": 1, ++ "kind": "assessment/v1", ++ "assessment_id": "sha256:3333333333333333333333333333333333333333333333333333333333333333", ++ "evidence_id": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", ++ "role": "contract", ++ "obligation_id": "req-repair-001", ++ "node_id": "repair-core", ++ "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", ++ "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ++ }, ++ { ++ "schema_version": 1, ++ "kind": "assessment/v1", ++ "assessment_id": "sha256:4444444444444444444444444444444444444444444444444444444444444444", ++ "evidence_id": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", ++ "role": "negative", ++ "obligation_id": "req-repair-001", ++ "node_id": "repair-core", ++ "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", ++ "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ++ } ++] +diff --git a/schemas/kernel/assessment/v1.schema.json b/schemas/kernel/assessment/v1.schema.json +new file mode 100644 +--- /dev/null ++++ b/schemas/kernel/assessment/v1.schema.json +@@ -0,0 +1,58 @@ ++{ ++ "$schema": "https://json-schema.org/draft/2020-12/schema", ++ "$id": "ospec://schemas/kernel/assessment/v1", ++ "title": "AssessmentV1", ++ "description": "Persistable assessment/binding distinct from evidence observation and verification verdict. Verdict is forbidden.", ++ "type": "object", ++ "schema_version": 1, ++ "required": [ ++ "schema_version", ++ "kind", ++ "assessment_id", ++ "evidence_id", ++ "role", ++ "obligation_id", ++ "node_id", ++ "candidate_id", ++ "policy_snapshot_id" ++ ], ++ "properties": { ++ "schema_version": { ++ "type": "integer", ++ "const": 1 ++ }, ++ "kind": { ++ "type": "string", ++ "const": "assessment/v1" ++ }, ++ "assessment_id": { ++ "type": "string", ++ "pattern": "^sha256:[a-f0-9]{64}$" ++ }, ++ "evidence_id": { ++ "type": "string", ++ "pattern": "^sha256:[a-f0-9]{64}$" ++ }, ++ "role": { ++ "type": "string", ++ "minLength": 1 ++ }, ++ "obligation_id": { ++ "type": "string", ++ "minLength": 1 ++ }, ++ "node_id": { ++ "type": "string", ++ "minLength": 1 ++ }, ++ "candidate_id": { ++ "type": "string", ++ "pattern": "^sha256:[a-f0-9]{64}$" ++ }, ++ "policy_snapshot_id": { ++ "type": "string", ++ "pattern": "^sha256:[a-f0-9]{64}$" ++ } ++ }, ++ "additionalProperties": false ++} +diff --git a/scripts/lib/independent-verifier/assessment.js b/scripts/lib/independent-verifier/assessment.js +new file mode 100644 +--- /dev/null ++++ b/scripts/lib/independent-verifier/assessment.js +@@ -0,0 +1,77 @@ ++"use strict"; ++ ++const path = require("node:path"); ++const { sha256Fingerprint } = require("../canonical-json.js"); ++const { validateInstance, loadSchemaById } = require("../kernel-schema-validator.js"); ++ ++const ASSESSMENT_V1_ID = "ospec://schemas/kernel/assessment/v1"; ++const DEFAULT_SCHEMA_ROOT = path.resolve(__dirname, "../../.."); ++const SHA256 = /^sha256:[a-f0-9]{64}$/; ++ ++let cachedAssessmentSchema = null; ++ ++function fail(reason_code, error) { ++ return { ok: false, reason_code, error: error || reason_code }; ++} ++ ++function getAssessmentSchema() { ++ if (!cachedAssessmentSchema) { ++ cachedAssessmentSchema = loadSchemaById(ASSESSMENT_V1_ID, { rootDir: DEFAULT_SCHEMA_ROOT }); ++ } ++ return cachedAssessmentSchema; ++} ++ ++function computeAssessmentId(fields) { ++ return sha256Fingerprint("assessment/v1", { ++ schema_version: fields.schema_version, ++ kind: fields.kind, ++ evidence_id: fields.evidence_id, ++ role: fields.role, ++ obligation_id: fields.obligation_id, ++ node_id: fields.node_id, ++ candidate_id: fields.candidate_id, ++ policy_snapshot_id: fields.policy_snapshot_id, ++ }); ++} ++ ++/** ++ * Emit an assessment/v1 binding. Verdict is forbidden. ++ * ++ * @param {object} input ++ * @returns {{ ok: true, assessment: object } | { ok: false, reason_code: string, error?: string }} ++ */ ++function emitAssessment(input) { ++ if (!input || typeof input !== "object") { ++ return fail("INVALID_ASSESSMENT", "assessment input must be an object"); ++ } ++ if (Object.prototype.hasOwnProperty.call(input, "verdict")) { ++ return fail("MIXED_ASSESSMENT_VERDICT", "assessment must not carry verdict"); ++ } ++ ++ const record = { ++ schema_version: 1, ++ kind: "assessment/v1", ++ evidence_id: input.evidence_id, ++ role: input.role, ++ obligation_id: input.obligation_id, ++ node_id: input.node_id, ++ candidate_id: input.candidate_id, ++ policy_snapshot_id: input.policy_snapshot_id, ++ }; ++ 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 }; ++} ++ ++module.exports = { ++ computeAssessmentId, ++ emitAssessment, ++}; +diff --git a/scripts/lib/independent-verifier/assessment.test.js b/scripts/lib/independent-verifier/assessment.test.js +new file mode 100644 +--- /dev/null ++++ b/scripts/lib/independent-verifier/assessment.test.js +@@ -0,0 +1,45 @@ ++"use strict"; ++ ++const assert = require("node:assert/strict"); ++const test = require("node:test"); ++ ++const { computeAssessmentId, emitAssessment } = require("./assessment.js"); ++ ++const BASE = { ++ schema_version: 1, ++ kind: "assessment/v1", ++ evidence_id: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", ++ obligation_id: "req-repair-001", ++ node_id: "repair-core", ++ candidate_id: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", ++ policy_snapshot_id: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", ++}; ++ ++test("REQ-independent-verification-006: computeAssessmentId includes role and obligation_id", () => { ++ const a = computeAssessmentId({ ...BASE, role: "acceptance" }); ++ const b = computeAssessmentId({ ...BASE, role: "invariants" }); ++ assert.match(a, /^sha256:[a-f0-9]{64}$/); ++ assert.notEqual(a, b); ++}); ++ ++test("REQ-independent-verification-006: emitAssessment validates and rejects verdict", () => { ++ const emitted = emitAssessment({ ...BASE, role: "acceptance" }); ++ assert.equal(emitted.ok, true, emitted.error); ++ assert.equal(emitted.assessment.kind, "assessment/v1"); ++ assert.equal(emitted.assessment.role, "acceptance"); ++ assert.equal(Object.prototype.hasOwnProperty.call(emitted.assessment, "verdict"), false); ++ ++ const withVerdict = emitAssessment({ ...BASE, role: "acceptance", verdict: "PASS" }); ++ assert.equal(withVerdict.ok, false); ++ assert.equal(withVerdict.reason_code, "MIXED_ASSESSMENT_VERDICT"); ++}); ++ ++test("REQ-independent-verification-006: four roles share evidence_id and produce four assessment_id values", () => { ++ const roles = ["acceptance", "invariants", "contract", "negative"]; ++ const emitted = roles.map((role) => emitAssessment({ ...BASE, role })); ++ assert.equal(emitted.every((item) => item.ok), true); ++ const evidenceIds = new Set(emitted.map((item) => item.assessment.evidence_id)); ++ const assessmentIds = new Set(emitted.map((item) => item.assessment.assessment_id)); ++ assert.equal(evidenceIds.size, 1); ++ assert.equal(assessmentIds.size, 4); ++}); +diff --git a/scripts/lib/independent-verifier/collector-provenance.js b/scripts/lib/independent-verifier/collector-provenance.js +new file mode 100644 +--- /dev/null ++++ b/scripts/lib/independent-verifier/collector-provenance.js +@@ -0,0 +1,84 @@ ++"use strict"; ++ ++const STRONG_CLASSES = Object.freeze(["runtime-observed", "host-attested", "tool-produced"]); ++const WEAK_CLASSES = Object.freeze(["model-reported", "human-decision", "external-unverified"]); ++ ++const ALLOWLIST = Object.freeze([ ++ { ++ ids: Object.freeze(["node-test", "npm-test", "node:test"]), ++ transport: "tool-execution-transport", ++ provenanceClass: "runtime-observed", ++ }, ++ { ++ ids: Object.freeze(["tool-execution"]), ++ transport: "tool-execution-transport", ++ provenanceClass: "tool-produced", ++ }, ++ { ++ ids: Object.freeze(["host-adapter"]), ++ transport: "execution-transport", ++ provenanceClass: "host-attested", ++ }, ++]); ++ ++function fail(reason_code, error) { ++ return { ok: false, reason_code, error: error || reason_code }; ++} ++ ++function deriveProvenanceClass(collector) { ++ if (!collector || typeof collector !== "object") { ++ return { provenance: null, trusted: false, worker: false }; ++ } ++ const id = collector.id; ++ const transport = collector.transport; ++ if (id === "worker" || transport === "worker-transport") { ++ return { provenance: "model-reported", trusted: false, worker: true }; ++ } ++ for (const row of ALLOWLIST) { ++ if (row.ids.includes(id) && row.transport === transport) { ++ return { provenance: row.provenanceClass, trusted: true, worker: false }; ++ } ++ } ++ return { provenance: null, trusted: false, worker: false }; ++} ++ ++/** ++ * Stored class comes from the harness collector channel (`provenanceClass` on the ++ * allowlist row), not from silently upgrading a payload claim. The allowlisted ++ * collector is the authority of the stored class even when the envelope claims a ++ * weaker provenance. `collectorResolution` is that channel result, not the raw ++ * collector object. ++ */ ++function resolveEvidenceProvenance(raw, harnessCollector) { ++ if (raw && Object.prototype.hasOwnProperty.call(raw, "collector")) return fail("UNTRUSTED_COLLECTOR"); ++ const claimed = raw && raw.provenance; ++ const collectorResolution = deriveProvenanceClass(harnessCollector); ++ const claimsStrong = STRONG_CLASSES.includes(claimed); ++ const claimedClass = typeof claimed === "string" && claimed.length > 0 ? claimed : null; ++ ++ if (claimsStrong && !collectorResolution.trusted) { ++ return fail("UNTRUSTED_COLLECTOR", "strong provenance requires an allowlisted collector/transport"); ++ } ++ if (collectorResolution.trusted) { ++ if (claimedClass && claimedClass !== collectorResolution.provenance) { ++ return fail("UNTRUSTED_COLLECTOR", "claimed provenance disagrees with collector-derived class"); ++ } ++ return { ok: true, provenance: collectorResolution.provenance }; ++ } ++ if (collectorResolution.worker) { ++ return { ok: true, provenance: "model-reported" }; ++ } ++ if (WEAK_CLASSES.includes(claimed)) { ++ return { ok: true, provenance: claimed }; ++ } ++ if (claimsStrong) { ++ return fail("UNTRUSTED_COLLECTOR", "strong provenance requires an allowlisted collector/transport"); ++ } ++ return fail("UNTRUSTED_COLLECTOR", "collector/transport metadata is absent or untrusted"); ++} ++ ++module.exports = { ++ STRONG_CLASSES, ++ deriveProvenanceClass, ++ resolveEvidenceProvenance, ++}; +diff --git a/scripts/lib/independent-verifier/obligation-coverage.js b/scripts/lib/independent-verifier/obligation-coverage.js +new file mode 100644 +--- /dev/null ++++ b/scripts/lib/independent-verifier/obligation-coverage.js +@@ -0,0 +1,118 @@ ++"use strict"; ++ ++const { emitAssessment } = require("./assessment.js"); ++const { evaluateProvenanceSufficiency } = require("./evidence.js"); ++ ++function fail(reason_code, error) { ++ return { ok: false, reason_code, error: error || reason_code }; ++} ++ ++function isApprovedDeferred(obligation) { ++ return Boolean( ++ obligation && ++ obligation.deferred && ++ typeof obligation.deferred === "object" && ++ typeof obligation.deferred.reason === "string" && ++ obligation.deferred.reason.trim() !== "" && ++ typeof obligation.deferred.approved_by === "string" && ++ obligation.deferred.approved_by.trim() !== "" ++ ); ++} ++ ++function isMust(obligation) { ++ return String((obligation && obligation.criticality) || "must").toLowerCase() === "must"; ++} ++ ++/** ++ * Walk non-deferred MUST obligations after strategy evaluation. ++ * Join key is persistable obligation_id, not K4a evidence tokens. ++ * ++ * @param {{ classified: object[], executionGraph: object, candidate: object, policySnapshotId: string }} input ++ * @returns {{ ok: true, assessments: object[] } | { ok: false, reason_code: string, error?: string }} ++ */ ++function walkMustObligations(input) { ++ const classified = Array.isArray(input && input.classified) ? input.classified : []; ++ const graph = input && input.executionGraph; ++ const candidate = input && input.candidate; ++ const policySnapshotId = input && input.policySnapshotId; ++ if (!graph || !Array.isArray(graph.obligations)) { ++ return fail("BINDING_MISMATCH", "executionGraph.obligations is required"); ++ } ++ const obligations = graph.obligations; ++ const byId = new Map(); ++ for (const obligation of obligations) { ++ if (obligation && typeof obligation.id === "string") { ++ byId.set(obligation.id, obligation); ++ } ++ } ++ ++ const assessments = []; ++ ++ for (const item of classified) { ++ const obligationIds = Array.isArray(item.obligation_ids) ? item.obligation_ids : []; ++ const nodeId = item.evidence && item.evidence.node_id; ++ for (const obligationId of obligationIds) { ++ const obligation = byId.get(obligationId); ++ if (!obligation) { ++ return fail("UNKNOWN_OBLIGATION_ID", `obligation_id ${obligationId} is not in the Obligation Manifest`); ++ } ++ const implementedBy = Array.isArray(obligation.implemented_by) ? obligation.implemented_by : []; ++ if (!implementedBy.includes(nodeId)) { ++ return fail( ++ "WRONG_IMPLEMENTING_NODE", ++ `node ${nodeId} does not implement obligation ${obligationId}` ++ ); ++ } ++ } ++ } ++ ++ for (const obligation of obligations) { ++ if (!isMust(obligation) || isApprovedDeferred(obligation)) continue; ++ ++ // Contract is non-empty presence of required_evidence, not token matching ++ // against its contents. Empty or missing list fails closed as UNFULFILLED_MUST. ++ const requiredEvidence = Array.isArray(obligation.required_evidence) ? obligation.required_evidence : []; ++ if (requiredEvidence.length === 0) { ++ return fail("UNFULFILLED_MUST", `MUST obligation ${obligation.id} has empty required_evidence`); ++ } ++ ++ const implementedBy = Array.isArray(obligation.implemented_by) ? obligation.implemented_by : []; ++ const matches = classified.filter((item) => { ++ const ids = Array.isArray(item.obligation_ids) ? item.obligation_ids : []; ++ return ids.includes(obligation.id) && implementedBy.includes(item.evidence && item.evidence.node_id); ++ }); ++ ++ const admissible = []; ++ for (const item of matches) { ++ const sufficiency = evaluateProvenanceSufficiency(item.evidence, { requireRuntime: true }); ++ if (sufficiency.ok) admissible.push(item); ++ } ++ if (admissible.length === 0) { ++ const hadWeak = matches.length > 0; ++ if (hadWeak) { ++ return fail("INSUFFICIENT_PROVENANCE", `MUST obligation ${obligation.id} lacks admissible provenance`); ++ } ++ return fail("UNFULFILLED_MUST", `MUST obligation ${obligation.id} has no admissible evidence`); ++ } ++ ++ for (const item of admissible) { ++ const emitted = emitAssessment({ ++ evidence_id: item.evidence.evidence_id, ++ role: item.role, ++ obligation_id: obligation.id, ++ node_id: item.evidence.node_id, ++ candidate_id: candidate && candidate.candidate_id, ++ policy_snapshot_id: policySnapshotId, ++ }); ++ if (!emitted.ok) return emitted; ++ assessments.push(emitted.assessment); ++ } ++ } ++ ++ return { ok: true, assessments }; ++} ++ ++module.exports = { ++ isApprovedDeferred, ++ walkMustObligations, ++}; +diff --git a/scripts/lib/independent-verifier/obligation-coverage.test.js b/scripts/lib/independent-verifier/obligation-coverage.test.js +new file mode 100644 +--- /dev/null ++++ b/scripts/lib/independent-verifier/obligation-coverage.test.js +@@ -0,0 +1,251 @@ ++"use strict"; ++ ++const assert = require("node:assert/strict"); ++const test = require("node:test"); ++ ++const { walkMustObligations } = require("./obligation-coverage.js"); ++ ++const CANDIDATE = { candidate_id: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" }; ++const POLICY = "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; ++const EVIDENCE = { ++ evidence_id: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", ++ provenance: "runtime-observed", ++ node_id: "repair-core", ++}; ++ ++function graph(obligations) { ++ return { obligations }; ++} ++ ++test("REQ-independent-verification-005: MUST without evidence fails UNFULFILLED_MUST", () => { ++ const result = walkMustObligations({ ++ classified: [], ++ executionGraph: graph([ ++ { ++ id: "req-repair-001", ++ criticality: "must", ++ implemented_by: ["repair-core"], ++ required_evidence: ["ev:test-pass"], ++ }, ++ ]), ++ candidate: CANDIDATE, ++ policySnapshotId: POLICY, ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "UNFULFILLED_MUST"); ++ assert.match(result.error, /req-repair-001/); ++}); ++ ++test("REQ-independent-verification-005: unknown obligation_id fails closed", () => { ++ const result = walkMustObligations({ ++ classified: [ ++ { role: "acceptance", evidence: EVIDENCE, obligation_ids: ["alien"] }, ++ ], ++ executionGraph: graph([ ++ { ++ id: "req-repair-001", ++ criticality: "must", ++ implemented_by: ["repair-core"], ++ required_evidence: ["ev:test-pass"], ++ }, ++ ]), ++ candidate: CANDIDATE, ++ policySnapshotId: POLICY, ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "UNKNOWN_OBLIGATION_ID"); ++}); ++ ++test("REQ-independent-verification-005: wrong implementing node fails closed", () => { ++ const result = walkMustObligations({ ++ classified: [ ++ { ++ role: "acceptance", ++ evidence: { ...EVIDENCE, node_id: "other-node" }, ++ obligation_ids: ["req-repair-001"], ++ }, ++ ], ++ executionGraph: graph([ ++ { ++ id: "req-repair-001", ++ criticality: "must", ++ implemented_by: ["repair-core"], ++ required_evidence: ["ev:test-pass"], ++ }, ++ ]), ++ candidate: CANDIDATE, ++ policySnapshotId: POLICY, ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "WRONG_IMPLEMENTING_NODE"); ++}); ++ ++test("REQ-independent-verification-005: approved deferral skips MUST", () => { ++ const result = walkMustObligations({ ++ classified: [], ++ executionGraph: graph([ ++ { ++ id: "req-repair-001", ++ criticality: "must", ++ implemented_by: ["repair-core"], ++ required_evidence: ["ev:test-pass"], ++ deferred: { reason: "later", approved_by: "maintainer" }, ++ }, ++ ]), ++ candidate: CANDIDATE, ++ policySnapshotId: POLICY, ++ }); ++ assert.equal(result.ok, true); ++ assert.deepEqual(result.assessments, []); ++}); ++ ++test("REQ-independent-verification-005: empty required_evidence on non-deferred MUST fails", () => { ++ const result = walkMustObligations({ ++ classified: [ ++ { role: "acceptance", evidence: EVIDENCE, obligation_ids: ["req-repair-001"] }, ++ ], ++ executionGraph: graph([ ++ { ++ id: "req-repair-001", ++ criticality: "must", ++ implemented_by: ["repair-core"], ++ required_evidence: [], ++ }, ++ ]), ++ candidate: CANDIDATE, ++ policySnapshotId: POLICY, ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "UNFULFILLED_MUST"); ++}); ++ ++test("REQ-independent-verification-005: strategy-shaped bindings still emit persistable assessments", () => { ++ const result = walkMustObligations({ ++ classified: [ ++ { role: "acceptance", evidence: EVIDENCE, obligation_ids: ["req-repair-001"] }, ++ ], ++ executionGraph: graph([ ++ { ++ id: "req-repair-001", ++ criticality: "must", ++ implemented_by: ["repair-core"], ++ required_evidence: ["ev:test-pass"], ++ }, ++ ]), ++ candidate: CANDIDATE, ++ policySnapshotId: POLICY, ++ }); ++ assert.equal(result.ok, true, result.error); ++ assert.equal(result.assessments.length, 1); ++ assert.equal(result.assessments[0].obligation_id, "req-repair-001"); ++ assert.equal(result.assessments[0].role, "acceptance"); ++}); ++ ++test("REQ-independent-verification-005: weak provenance on MUST is INSUFFICIENT_PROVENANCE", () => { ++ const runtimeUnbound = { ++ ...EVIDENCE, ++ evidence_id: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ++ }; ++ const weakBound = { ...EVIDENCE, provenance: "model-reported" }; ++ const result = walkMustObligations({ ++ classified: [ ++ { role: "characterization-before", evidence: runtimeUnbound, obligation_ids: ["req-char-001"] }, ++ { role: "acceptance", evidence: weakBound, obligation_ids: ["req-repair-001"] }, ++ ], ++ executionGraph: graph([ ++ { ++ id: "req-repair-001", ++ criticality: "must", ++ implemented_by: ["repair-core"], ++ required_evidence: ["ev:test-pass"], ++ }, ++ { ++ id: "req-char-001", ++ criticality: "should", ++ implemented_by: ["repair-core"], ++ required_evidence: ["ev:char"], ++ }, ++ ]), ++ candidate: CANDIDATE, ++ policySnapshotId: POLICY, ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "INSUFFICIENT_PROVENANCE"); ++ assert.match(result.error, /req-repair-001/); ++ assert.equal(result.assessments, undefined); ++}); ++ ++test("REQ-independent-verification-005: second unfulfilled MUST is identified", () => { ++ const result = walkMustObligations({ ++ classified: [ ++ { role: "acceptance", evidence: EVIDENCE, obligation_ids: ["req-repair-001"] }, ++ ], ++ executionGraph: graph([ ++ { ++ id: "req-repair-001", ++ criticality: "must", ++ implemented_by: ["repair-core"], ++ required_evidence: ["ev:test-pass"], ++ }, ++ { ++ id: "req-repair-002", ++ criticality: "must", ++ implemented_by: ["repair-core"], ++ required_evidence: ["ev:second"], ++ }, ++ ]), ++ candidate: CANDIDATE, ++ policySnapshotId: POLICY, ++ }); ++ assert.equal(result.ok, false); ++ assert.equal(result.reason_code, "UNFULFILLED_MUST"); ++ assert.match(result.error, /req-repair-002/); ++ assert.equal(result.assessments, undefined); ++}); ++ ++test("REQ-independent-verification-005: incomplete deferral still requires MUST coverage", () => { ++ const base = { ++ id: "req-repair-001", ++ criticality: "must", ++ implemented_by: ["repair-core"], ++ required_evidence: ["ev:test-pass"], ++ }; ++ const incomplete = [ ++ { reason: "later" }, ++ { approved_by: "maintainer" }, ++ { reason: "later", approved_by: " " }, ++ { reason: " ", approved_by: "maintainer" }, ++ ]; ++ for (const deferred of incomplete) { ++ const result = walkMustObligations({ ++ classified: [], ++ executionGraph: graph([{ ...base, deferred }]), ++ candidate: CANDIDATE, ++ policySnapshotId: POLICY, ++ }); ++ assert.equal(result.ok, false, JSON.stringify(deferred)); ++ assert.equal(result.reason_code, "UNFULFILLED_MUST"); ++ assert.match(result.error, /req-repair-001/); ++ assert.match(result.error, /evidence/); ++ assert.doesNotMatch(result.error, /assessment/i); ++ } ++}); ++ ++test("REQ-independent-verification-005: missing executionGraph fails closed", () => { ++ const missing = walkMustObligations({ ++ classified: [], ++ candidate: CANDIDATE, ++ policySnapshotId: POLICY, ++ }); ++ assert.equal(missing.ok, false); ++ assert.equal(missing.reason_code, "BINDING_MISMATCH"); ++ ++ const nonArray = walkMustObligations({ ++ classified: [], ++ executionGraph: { obligations: { id: "req-repair-001" } }, ++ candidate: CANDIDATE, ++ policySnapshotId: POLICY, ++ }); ++ assert.equal(nonArray.ok, false); ++ assert.equal(nonArray.reason_code, "BINDING_MISMATCH"); ++}); diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/apply-progress.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/apply-progress.md new file mode 100644 index 00000000..c0cd1ef3 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/apply-progress.md @@ -0,0 +1,100 @@ +# Apply Progress: k6b-verification-integrity-remediation + +**Mode:** Focused TDD (`testing.tdd_mode: focused`) +**Delivery:** `size:exception` (maintainer-accepted oversized single PR; Phases 1–8 in one batch) +**Batch:** 1 (first apply; no prior progress) +**Branch:** `feat/k6b-verifier-evidence-assurance-graph` +**Verified at:** 2026-08-27T14:24:10Z + +## Batch 1 — Phases 1–8 (complete) + +All 30 tasks implemented and locally verified. + +### Local verification + +```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 \ + scripts/lib/k2a-maturity-docs.test.js +→ 65 pass, 0 fail + +node --test scripts/lib/contract-checkers/k1-schema-compat.test.js \ + scripts/lib/k1-scope-guard.test.js \ + scripts/lib/kernel-schema-fixtures.test.js \ + scripts/lib/roadmap-boundary.test.js +→ 34 pass, 0 fail +``` + +v2/K1 schema pins remain byte-identical (`evidence/v2`, `verification/v2`, K1 baseline). + +### TDD Cycle Evidence + +| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR | Notes / Rationale | +| ---- | --- | ----- | ---- | --- | ----- | ----- | ----- | ----- | +| 1.1–1.5 | `scripts/lib/k6b-schema-fixtures.test.js` | contract | schema fixtures + digest pins | [x] | [x] | [x] | [x] | assessment/v1 family; four-role distinct ids; v2/K1 pins frozen | +| 2.1–2.3 | `scripts/lib/independent-verifier/assessment.test.js` | unit | schema validate | [x] | [x] | [x] | [x] | `computeAssessmentId` includes role+obligation; verdict rejected | +| 3.1–3.4 | `scripts/lib/independent-verifier/index.test.js` | unit | allowlist mapper | [x] | [x] | [x] | [x] | payload-only strong → `UNTRUSTED_COLLECTOR`; collector ≠ digest | +| 4.1–4.3 | `scripts/lib/independent-verifier/obligation-coverage.test.js` | unit | MUST walk | [x] | [x] | [x] | [x] | `UNFULFILLED_MUST` / `UNKNOWN_OBLIGATION_ID` / `WRONG_IMPLEMENTING_NODE`; empty `required_evidence` fails | +| 4R S2 | `scripts/lib/independent-verifier/obligation-coverage.test.js` | unit | 6 pass | lock | [x] | [x] | [x] | `INSUFFICIENT_PROVENANCE`; 2nd MUST; deferral incompleto; grafo ausente `BINDING_MISMATCH` | +| 5.1–5.4 | `scripts/lib/independent-verifier/index.test.js` | integration | facade stub | [x] | [x] | [x] | [x] | projector stub → `GRAPH_PROJECTION_FAILED`; unique-sort still one E | +| 6.1–6.5 | `scripts/lib/assurance-graph/index.test.js` | unit | canonical `graph_id` | [x] | [x] | [x] | [x] | kind/namespace `rejectForbidden`; replay byte-identical; C1→C2 → `GRAPH_DIVERGENCE` | +| 7.1–7.4 | `scripts/k6b-verifier-assurance-graph-e2e.test.js` | e2e | persistable replay | [x] | [x] | [x] | [x] | assessments + `canonical_inputs` persisted; adversarial cases in index tests | +| 8.1–8.2 | `scripts/lib/k2a-maturity-docs.test.js` | docs | status table | [x] | [x] | n/a | n/a | K6b `revise`; K6c `blocked-by-K6b-remediation` | + +### Task status + +- [x] 1.1–1.5 Assessment/v1 schema publication +- [x] 2.1–2.3 Assessment identity module +- [x] 3.1–3.4 Collector provenance allowlist +- [x] 4.1–4.3 Obligation Manifest MUST walk +- [x] 5.1–5.4 Verifier facade integration +- [x] 6.1–6.5 Assurance Graph remediation +- [x] 7.1–7.4 Adversarial tests and E2E +- [x] 8.1–8.2 Roadmap documentation + +### Deviations from design + +None — implementation matches design. Companion updates required by existing tests: + +- `v1-four-roles.json` is an array of four payloads; `k1-schema-compat` skips it via `fixtureNameFilter` (same pattern as `k3-frozen.json`) so the generic object walker does not treat the bundle as one instance. +- `schemas/kernel/assessment/` excluded from K1 frozen inventory (`k1-compat.js`, `k1-scope-guard.test.js` successor prefixes). +- `k2a-maturity-docs.test.js` updated to pin K6b `revise` / K6c `blocked-by-K6b-remediation`. + +### Issues found + +None blocking. Existing verifier/E2E tests that claimed strong provenance without collector now supply an allowlisted collector (or omit collector to assert `UNTRUSTED_COLLECTOR`). + +## Batch 2 — 4R slice `S-ea4088e8a61de9f8` (collector-trust-boundary) + +**Lineage:** `sha256:262dda4ab0b3ec0fe60b7db34683c55d3c4d2590fe69282c40197cbc95aacf42` +**Delivery:** `exception-ok` / this slice only. Forecast cap 180. + +Closed `F-d5739d79237afeb8` (CRITICAL): weak claim + allowlisted collector now fails `UNTRUSTED_COLLECTOR`; claim↔collector disagreement is symmetric. + +Co-located WARNINGs remediated in-cap: collector mapping tests (`F-2fc6db350f5b8afc`); STALE remint (`F-ad61b7e3cff9629a`); canonicalInputs bind (`F-6b1f8c8265c82b3e`); GRAPH_DIVERGENCE facade (`F-990aa817913b8273`); `rejectForbidden` kind-first + comments (`F-99f9c70bdae46c12`); projector satisfies comment (`F-eb2d325d6d801a14`); documented `derived` vs collector resolution (`F-4839a9a36f0b55be`). + +### Local verification + +```text +node --test scripts/lib/independent-verifier/index.test.js \ + scripts/lib/assurance-graph/index.test.js \ + scripts/k6b-verifier-assurance-graph-e2e.test.js +→ 48 pass, 0 fail +``` + +## Batch 3 — 4R retry 2 `S-ea4088e8a61de9f8` +- [x] F-d5739d79237afeb8: envelope `collector` fails closed; harness `input.collector` derives class. GREEN: `node --test --test-name-pattern "envelope collector fails closed" scripts/lib/independent-verifier/index.test.js` + +## Batch 4 — 4R `S-ad5558b5639b6890` +- [x] F-b3d6518c12aa69fe / F-00f97ff647d28eea / F-ef73f7e16cab6436 + colocated F-2be19c4683d81ba1 / F-9d6a187e3d18dbf5 +- Safety net 6 pass; after 10 pass / 0 fail: `node --test scripts/lib/independent-verifier/obligation-coverage.test.js` + +## Batch 5 — successor `new-candidate` (helpers + remaining WARNINGs) +- [x] Harness `input.collector` (optional `input.collectors[]`); envelope `collector` still fail-closed +- [x] F-2fc6db350f5b8afc mapping `npm-test`/`node:test`; F-4839a9a36f0b55be `provenanceClass`; F-f979f00ae92cda6f comment +- GREEN: `node --test scripts/lib/independent-verifier/index.test.js scripts/lib/independent-verifier/obligation-coverage.test.js scripts/lib/assurance-graph/index.test.js scripts/k6b-verifier-assurance-graph-e2e.test.js` → 59 pass / 0 fail +- Successor freeze: 0 BLOCKER, 0 CRITICAL, 6 WARNING (advisory, already closed in-tree: FABRICATED_EVIDENCE tests, INVALID_ASSESSMENT, comments/renames), 1 SUGGESTION diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/archive-plan.json b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/archive-plan.json new file mode 100644 index 00000000..653243fa --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/archive-plan.json @@ -0,0 +1,102 @@ +{ + "schema_version": 1, + "change": "k6b-verification-integrity-remediation", + "source_fingerprint": "sha256:e3d7ac6dc6507a643fcf8c96f6e7acaa43cbcefcd7ec385aa00d571cdcd71139", + "spec_writes": [ + { + "domain": "independent-verification", + "source_delta": "prepared-specs/independent-verification/spec.md", + "target": "openspec/specs/independent-verification/spec.md", + "target_before_sha256": "sha256:8063703bb9860b0fbff8b49894fa17932c222cb58f1dea028bda57a00330fc6d", + "content_sha256": "sha256:3207ce8b5b280472b9b505378cf91df22b1ae22e007adb9dbf8a173405c5b0a0" + }, + { + "domain": "assurance-graph", + "source_delta": "prepared-specs/assurance-graph/spec.md", + "target": "openspec/specs/assurance-graph/spec.md", + "target_before_sha256": "sha256:c7d171aab463f80f504aff6706e7847ef44a849ae79a8e06f13a8e4e17a1795d", + "content_sha256": "sha256:81f10f2046ae5519aead34f0f945275e99bfefbc03f9a898d80ce8bcbc437f56" + }, + { + "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:fabcbc4edb0ab2934414ca9bf5bca0f88c7e5a3759a0c427f739d0475835d25d", + "content_sha256": "sha256:0f6c1f85aaff2f9b7a60320a66830bbc23feeb01a29ce4e6a8ef8f5d59ef4cec" + } + ], + "adr_promotions": [ + { + "source": "decisions/adr-001.md", + "target": "docs/adr/adr-20260827-007-additive-assessment-family-id.md", + "content_sha256": "sha256:4fce74c687bfda4d3b7cb60ed2faf83d075f7a46606983a1018528086560b109" + }, + { + "source": "decisions/adr-002.md", + "target": "docs/adr/adr-20260827-008-must-walk-after-strategy-assessment-identity.md", + "content_sha256": "sha256:66bbf61c8cc81d825c5080e7c35f410f995c5ea606890bc565d0f4b03b61009c" + }, + { + "source": "decisions/adr-003.md", + "target": "docs/adr/adr-20260827-009-collector-transport-provenance-allowlist.md", + "content_sha256": "sha256:292afdb2da2f9cc6a27de600b8af70cf0fb0f4f42f587cb55de29dc18d34cd72" + }, + { + "source": "decisions/adr-004.md", + "target": "docs/adr/adr-20260827-010-canonical-graph-id-fail-closed-projection.md", + "content_sha256": "sha256:a308aaa6935cc85bdd416ca06b05cc637c14695a52ebb2cc2ee5ab38f16b15d6" + } + ], + "archive_inventory": [ + ".4r/create-successor.js", + ".4r/derived.json", + ".4r/evidence.json", + ".4r/fail-and-retry-slice1.js", + ".4r/freeze-lineage.js", + ".4r/generalist.json", + ".4r/lens-readability-findings.json", + ".4r/lens-reliability-findings.json", + ".4r/lens-resilience-findings.json", + ".4r/lens-risk-findings.json", + ".4r/lineage.json", + ".4r/manifest-v2.json", + ".4r/migrate-and-begin-slice1.js", + ".4r/normalize-evidence.js", + ".4r/pass-slice1-begin-slice2.js", + ".4r/pass-slice2.js", + ".4r/persist-gate.js", + ".4r/planned-gate.json", + ".4r/planned-lineage.json", + ".4r/predecessor-lineage.json", + ".4r/record-lenses.js", + ".4r/record-slice1-retry2.js", + ".4r/record-slice1.js", + ".4r/record-slice2.js", + ".4r/unified.diff", + "apply-progress.md", + "archive-report.md", + "decisions/adr-001.md", + "decisions/adr-002.md", + "decisions/adr-003.md", + "decisions/adr-004.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": [ + "verify-suggestion-001 SUGGESTION: Worker-collector MUST case accepts INSUFFICIENT_PROVENANCE or UNTRUSTED_COLLECTOR; pinning one code would sharpen contract.", + "verify-suggestion-002 SUGGESTION: Mismatched canonicalInputs accepts GRAPH_DIVERGENCE or BINDING_MISMATCH; design prefers GRAPH_DIVERGENCE.", + "4R advisory WARNINGs (FABRICATED_EVIDENCE tests, INVALID_ASSESSMENT, comments/renames) remain non-blocking; covering runtime tests pass." + ], + "rollback": { + "strategy": "staging-rename" + } +} diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/archive-report.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/archive-report.md new file mode 100644 index 00000000..478ec8bd --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/archive-report.md @@ -0,0 +1,75 @@ +# Archive Report: k6b-verification-integrity-remediation + +**Archive destination (planned)**: `openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/` +**Verified**: 2026-08-27 +**Verify verdict**: PASS (re-verify after 4R successor; 35/35 MUST scenarios; focal 75/75; `npm test` 2754 pass / 0 fail) + +## Summary + +Remediation change closes six K6b verification-integrity defects: Obligation Manifest MUST coverage after strategy evaluation, persistable assessment/binding distinct from evidence/v2, collector-derived strong provenance, canonical `graph_id` fingerprinting, fail-closed facade projection, and `rejectForbidden` by kind/namespace. Additive kernel family `assessment/v1` preserves evidence/v2, verification/v2, and K1 v1 byte pins. Implementation spans `scripts/lib/independent-verifier/`, `scripts/lib/assurance-graph/`, kernel schemas/fixtures, E2E replay tests, and harness-evolution roadmap docs. All 30 tasks complete under approved `size:exception` delivery. Candidate excludes `models.yaml`. + +## Verification Gate + +| Check | Result | +|-------|--------| +| Verify verdict | PASS | +| CRITICAL issues | None | +| WARNING issues (verify) | None | +| SUGGESTION issues (verify) | 2 (non-blocking; recorded below) | +| Apply tasks complete | 30/30 | +| 4R review gate | approved (`archive_allowed: true`; successor lineage generation 2) | +| Successor lineage (read-only) | `sha256:a051818ce2bb310c5fa3a29c8a7b730a564dd5b44ff1da238a73089fcce94c02` | +| Predecessor lineage (read-only) | `sha256:262dda4ab0b3ec0fe60b7db34683c55d3c4d2590fe69282c40197cbc95aacf42` | +| Baseline fingerprints | Match `state.yaml` for all three delta domains | + +## Spec Preparation (change-local) + +| Domain | Action | Added | Modified | Removed | +|--------|--------|-------|----------|---------| +| `independent-verification` | Prepared merge | REQ-005, REQ-006, REQ-007 (3) | REQ-003, REQ-004 (2) | — | +| `assurance-graph` | Prepared merge | REQ-005, REQ-006 (2) | REQ-001, REQ-002 (2) | — | +| `kernel-contract-schemas` | Prepared merge | REQ-027 (1) | REQ-001 (1) | — | + +Prepared bytes: + +- `prepared-specs/independent-verification/spec.md` (`sha256:3207ce8b5b280472b9b505378cf91df22b1ae22e007adb9dbf8a173405c5b0a0`) +- `prepared-specs/assurance-graph/spec.md` (`sha256:81f10f2046ae5519aead34f0f945275e99bfefbc03f9a898d80ce8bcbc437f56`) +- `prepared-specs/kernel-contract-schemas/spec.md` (`sha256:0f6c1f85aaff2f9b7a60320a66830bbc23feeb01a29ce4e6a8ef8f5d59ef4cec`) + +Live `openspec/specs/**` writes are runtime-owned. + +## ADR Promotions (planned) + +| Source | Planned target | +|--------|----------------| +| `decisions/adr-001.md` | `docs/adr/adr-20260827-007-additive-assessment-family-id.md` | +| `decisions/adr-002.md` | `docs/adr/adr-20260827-008-must-walk-after-strategy-assessment-identity.md` | +| `decisions/adr-003.md` | `docs/adr/adr-20260827-009-collector-transport-provenance-allowlist.md` | +| `decisions/adr-004.md` | `docs/adr/adr-20260827-010-canonical-graph-id-fail-closed-projection.md` | + +Change-local copies under `decisions/` travel with the archive folder as audit trail. + +## Accepted Risks / Follow-ups + +| ID | Severity | Owner | Summary | Disposition | +|----|----------|-------|---------|-------------| +| verify-suggestion-001 | SUGGESTION | independent-verifier | Worker-collector MUST case accepts `INSUFFICIENT_PROVENANCE` or `UNTRUSTED_COLLECTOR`; pinning one code would sharpen contract | Non-blocking follow-up | +| verify-suggestion-002 | SUGGESTION | assurance-graph | Mismatched `canonicalInputs` accepts `GRAPH_DIVERGENCE` or `BINDING_MISMATCH`; design prefers `GRAPH_DIVERGENCE` | Non-blocking follow-up | + +4R advisory WARNINGs (FABRICATED_EVIDENCE tests, INVALID_ASSESSMENT, comments/renames) remain advisory. Covering runtime tests pass; not escalated to verify CRITICAL and not archive blockers. + +## Archive Inventory + +Origin paths preserved by the planned runtime move (excluding `archive-plan.json` from fingerprint identity). Includes proposal, design, tasks, apply/verify/archive reports, delta and prepared specs, four decisions, state, `.4r/` review lineage artifacts, and remediation scripts (42 entries at plan emission). + +## Runtime Completion (pending) + +- Live spec merge and ADR promotion: `node scripts/archive-transaction-run.js k6b-verification-integrity-remediation` +- Source directory `openspec/changes/k6b-verification-integrity-remediation/` still exists until runtime receipt confirms full match and delete-after-commit. + +## Cost + +No per-phase cost data was recorded for this change +(`.ospec/session/k6b-verification-integrity-remediation/phase-costs.jsonl` missing or empty). + +**Total user questions asked**: 0 diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-001.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-001.md new file mode 100644 index 00000000..ec76618e --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-001.md @@ -0,0 +1,23 @@ +# ADR-001: Additive assessment family `$id` + +- Status: proposed +- Change: k6b-verification-integrity-remediation +- Date: 2026-08-27 + +## Context + +Assumption `sdd-propose-001` left the persistable binding `$id` to design. Intent forbids in-place mutation of `evidence/v2` and K1 v1. Assessment identity must include `role` and `obligation_id` without collapsing physical `EvidenceId`. + +## Decision + +Publish family `assessment` at `$id` `ospec://schemas/kernel/assessment/v1`, `kind: "assessment/v1"`, path `schemas/kernel/assessment/v1.schema.json`, `schema_version: 1`. Register additively in `manifest.json` and `contract-claims.json`. Keep `evidence/v2`, `verification/v2`, and K1 v1 bytes/pins frozen. + +## Alternatives + +- Mutate `evidence/v2` to carry role/obligation: rejected; observation identity would absorb evaluation identity. +- Evolve `verification/v3` with embedded bindings: rejected; verdict unique-sort of `evidence_ids` still hides distinct roles. +- `$id` `ospec://schemas/kernel/assessment-binding/v1`: rejected; breaks the `{noun}/vN` kernel `$id` pattern. + +## Consequences + +Consumers pin a third K6b-era family without migrating evidence. Rollback deletes the additive family. Four roles over one observation become four `assessment_id` values by construction. diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-002.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-002.md new file mode 100644 index 00000000..4137374f --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-002.md @@ -0,0 +1,23 @@ +# ADR-002: MUST walk after strategy; assessment identity ≠ EvidenceId + +- Status: proposed +- Change: k6b-verification-integrity-remediation +- Date: 2026-08-27 + +## Context + +`evaluateStrategy()` only checks role minimums. PASS was therefore possible without Obligation Manifest coverage. `obligation_ids` on classified evidence are ephemeral, so `satisfies` edges cannot be replayed from `evidence/v2`. + +## Decision + +Keep `evaluateStrategy` as the role-shape gate. After it succeeds, walk every non-deferred `must` obligation, emit `assessment/v1` records, and require those assessments for PASS. `computeEvidenceId` stays observation-only. `assessment_id` fingerprints `evidence_id`, `role`, `obligation_id`, `node_id`, `candidate_id`, and `policy_snapshot_id`. + +## Alternatives + +- Fold MUST coverage into `evaluateStrategy`: rejected; mixes strategy policy with graph obligations and reason codes. +- Walk obligations before strategy: rejected; delays cheap role failures. +- Treat unique-sort of `verification.evidence_ids` as binding identity: rejected; one EvidenceId used as four roles would collapse. + +## Consequences + +Reason codes split cleanly (`MISSING_STRATEGY_MINIMUM` vs `UNFULFILLED_MUST` / `UNKNOWN_OBLIGATION_ID` / `WRONG_IMPLEMENTING_NODE`). Projector `satisfies` edges consume assessments only. Existing tests that omit obligation bindings will fail closed until updated. diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-003.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-003.md new file mode 100644 index 00000000..12cc7e75 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-003.md @@ -0,0 +1,23 @@ +# ADR-003: Strong provenance from collector/transport allowlist + +- Status: proposed +- Change: k6b-verification-integrity-remediation +- Date: 2026-08-27 + +## Context + +`normalizeEvidence` currently copies `raw.provenance` into `evidence/v2`. A worker can claim `runtime-observed`. PKI is out of scope. Payload digest identifies bytes, not origin. + +## Decision + +Derive strong classes (`runtime-observed`, `host-attested`, `tool-produced`) from harness-supplied `collector.id` + `transport` via a fail-closed allowlist (`node-test`/`npm-test`/`node:test` → runtime-observed; `tool-execution` → tool-produced; `host-adapter` → host-attested). Worker/absent/unknown collectors cannot produce a strong class. Store the derived class on `evidence/v2`; never copy collector metadata onto that record. + +## Alternatives + +- Trust the payload provenance string: rejected; this is the verified defect. +- Require signatures or PKI: rejected; out of scope for this change. +- Treat payload digest as origin: rejected; digest is content identity only. + +## Consequences + +Callers that claim a strong class must pass allowlisted collector metadata (tests included). Mis-mapped collector ids fail closed rather than silently weakening provenance. diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-004.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-004.md new file mode 100644 index 00000000..595329bc --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/decisions/adr-004.md @@ -0,0 +1,23 @@ +# ADR-004: Canonical graph_id fingerprint and fail-closed projection + +- Status: proposed +- Change: k6b-verification-integrity-remediation +- Date: 2026-08-27 + +## Context + +`graph_id` currently hashes only `candidate_id` plus canonical nodes/edges. Contract or policy changes do not diverge. `verifyCandidate` returns `ok: true` when projection fails, omitting the graph. `rejectForbidden` substring-matches `id`, so `REQ-add-authorization-header` is rejected. + +## Decision + +Fingerprint `contract_digest`, `policy_snapshot_id`, Execution Graph digest, canonical OpenSpec input, `candidate_id`, and canonical nodes/edges in `graph_id`. Persist those digests as optional `canonical_inputs` on `assurance-graph/v1`. `verifyCandidate` requires a successful projection before `ok: true`. Match forbidden subjects by `kind`/`namespace`, never by `id` substring. + +## Alternatives + +- Keep the current `graph_id` preimage: rejected; replay cannot detect contract/policy drift. +- Leave projection optional on the facade: rejected; PASS without a graph is the integrity hole. +- Continue substring markers on `id`: rejected; legitimate requirement ids containing `authorization` fail. + +## Consequences + +Existing runtime `graph_id` values change. Replay becomes possible from persistable outputs. Facade tests that ignored projector failure must assert `GRAPH_PROJECTION_FAILED` / `GRAPH_DIVERGENCE`. diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/design.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/design.md new file mode 100644 index 00000000..202c6999 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/design.md @@ -0,0 +1,210 @@ +# Design: k6b-verification-integrity-remediation + +## Technical Approach + +Remediate six K6b integrity defects in the existing CommonJS verifier and projector. Do not reopen the macro-slice. `evaluateStrategy()` stays a role-shape gate. A new MUST walk over the Obligation Manifest, persistable `assessment/v1` bindings, collector-derived provenance, canonical `graph_id` fingerprinting, fail-closed projection, and kind/namespace `rejectForbidden` close the gaps. `evidence/v2` remains the physical observation; `verification/v2` remains the verdict. Roadmap status is a documentation edit only. + +Resolves assumption `sdd-propose-001` via ADR-001. + +## Architecture Decisions + +### Decision: Additive assessment family `$id` (ADR-001) + +| Option | Tradeoff | Decision | +| --- | --- | --- | +| Mutate `evidence/v2` with `role` / `obligation_id` | Collapses observation identity; forbidden by intent. | Rejected | +| Evolve `verification/v3` to embed bindings | Unique-sort of `evidence_ids` still hides roles; mutates verdict family. | Rejected | +| `ospec://schemas/kernel/assessment-binding/v1` | Explicit but breaks `{noun}/vN` `$id` pattern. | Rejected | +| `ospec://schemas/kernel/assessment/v1` | Additive, pinnable, distinct `kind`. | **Chosen** | + +**Rationale**: Matches `evidence/v2` and `verification/v2`. Manifest key `assessment` (first version, like `assurance-graph`). `kind: "assessment/v1"`. Path `schemas/kernel/assessment/v1.schema.json`. + +### Decision: MUST walk after strategy, assessments ≠ EvidenceId (ADR-002) + +| Option | Tradeoff | Decision | +| --- | --- | --- | +| Fold obligations into `evaluateStrategy` | Mixes role policy with graph coverage; harder to reason codes. | Rejected | +| Walk MUST first | Wastes work when strategy already fails. | Rejected | +| Strategy then MUST walk; assessment id includes role+obligation | Fail-fast role check; persistable coverage. | **Chosen** | + +**Rationale**: Strategy satisfied ≠ Execution Graph satisfied. `computeEvidenceId` is unchanged (no role). Assessment identity fingerprints role, obligation, node, evidence, candidate, and policy snapshot. + +### Decision: Collector/transport allowlist, no PKI (ADR-003) + +| Option | Tradeoff | Decision | +| --- | --- | --- | +| Trust payload `provenance` | Current defect; worker can claim `runtime-observed`. | Rejected | +| Require signatures / PKI | Out of scope. | Rejected | +| Harness-supplied collector+transport allowlist | Fail-closed; digest ≠ origin. | **Chosen** | + +**Rationale**: Strong class is derived from the channel the harness invoked. Unknown or absent collector cannot produce a strong class. + +### Decision: Canonical `graph_id` + facade fail-closed (ADR-004) + +| Option | Tradeoff | Decision | +| --- | --- | --- | +| Keep `graph_id` = candidate+nodes+edges | Contract/policy drift is invisible. | Rejected | +| Return `ok: true` without a graph | Current facade defect. | Rejected | +| Fingerprint canonicalInputs; `ok: true` requires projection | Replayable; fail-closed. | **Chosen** | + +**Rationale**: `satisfies` edges rebuild from persistable assessments, never from ephemeral `obligation_ids`. + +## Data Flow + +### Verification sequence (MUST walk vs strategy) + +```text +Caller verifyCandidate strategy-policy obligation-coverage projector + | verify(input) | | | | + |---------------------->| validateBindings | | | + | | selectStrategy------->| | | + | | normalizeEvidence | collector → class | | + | | evaluateStrategy----->| roles only | | + | | | ok ≠ graph covered | | + | | walk MUST-------------------------------------->| | + | | | skip approved deferred | | + | | | emit assessment/v1 | | + | | projectAssuranceGraph------------------------------------------------>| + | | | graph_id includes canonicalInputs | + | | | satisfies ← assessments only | + | | if !projected.ok → ok:false, no PASS, no graph | + | | emit verification/v2 (unique-sort evidence_ids still one E) | + |<----------------------| evidence, assessments, verification, assurance_graph | +``` + +**Order**: bindings → strategy selection → normalize (collector class) → `evaluateStrategy` → MUST walk → project → (optional reconcile) → emit verdict. Strategy failure short-circuits; MUST walk never upgrades a failed strategy. + +**Deferred**: skip a `must` item only when `deferred.reason` and `deferred.approved_by` are both non-empty (same predicate as K4a `obligation-manifest.js`). Partial deferral is not deferred. `should` / `may` are not required for PASS. + +**Coverage unit**: a non-deferred MUST is satisfied when ≥1 persistable assessment binds admissible evidence to that `obligation_id` and a `node_id` ∈ `implemented_by`. Join key is `obligation_id`, not echoing K4a tokens such as `ev:test-pass` onto `evidence/v2`. Any binding whose `node_id` is outside `implemented_by` fails closed even if another binding is valid. Empty `required_evidence` on a non-deferred MUST fails closed. + +| Condition | reason_code | +| --- | --- | +| Non-deferred MUST with no admissible assessment | `UNFULFILLED_MUST` (identifies `obligation_id`) | +| Binding `obligation_id` absent from manifest | `UNKNOWN_OBLIGATION_ID` | +| Binding `node_id` ∉ `implemented_by` | `WRONG_IMPLEMENTING_NODE` | +| Strong class claimed/needed without trusted collector | `UNTRUSTED_COLLECTOR` | +| Weak/model class vs runtime obligation | `INSUFFICIENT_PROVENANCE` | +| Projection cannot materialize | `GRAPH_PROJECTION_FAILED` | +| Stored graph ≠ recompute from persistable inputs | `GRAPH_DIVERGENCE` | + +### Collector mapping (strong provenance) + +| `collector.id` + `transport` | Derived class | +| --- | --- | +| `node-test` \| `npm-test` \| `node:test` + `tool-execution-transport` | `runtime-observed` | +| `tool-execution` + `tool-execution-transport` | `tool-produced` | +| `host-adapter` + `execution-transport` | `host-attested` | +| `worker` / `worker-transport` / absent / unknown | never strong (`model-reported` if worker; else fail for strong claims) | + +Stored `evidence.provenance` is the derived class. Payload digest is content identity, not origin. Payload string `runtime-observed` without a matching allowlisted collector → `UNTRUSTED_COLLECTOR`. No PKI. + +### `graph_id` preimage and replay + +```text +graph_id = sha256Fingerprint("assurance-graph/v1", { + candidate_id, + contract_digest, + policy_snapshot_id, + execution_graph_digest, // executionGraph.graph_id + openspec_input_digest, // fingerprint of canonical OpenSpec/Git input + nodes, edges // canonical sorted sets +}) +``` + +Persist `canonical_inputs` on the projected graph (additive optional on `assurance-graph/v1`). Replay calls `projectAssuranceGraph` with stored assessments, evidence, verification, and those digests — never ephemeral projector `obligation_ids`. Changing any canonical input yields a new `graph_id`; reconcile of the old graph returns `GRAPH_DIVERGENCE`. + +`rejectForbidden`: allow-list `kind`; reject forbidden `kind` / optional in-memory `namespace` (`finding`, `attestation`, `authorization`, `evaluation-attestation`). Never scan `id`. `REQ-add-authorization-header` with `kind: "requirement"` stays valid. + +`verifyCandidate`: if projection fails, return `ok: false` with `GRAPH_PROJECTION_FAILED` (or `GRAPH_DIVERGENCE` on stored mismatch), omit `assurance_graph`, do not emit `PASS` / `PASS WITH WARNINGS`. Projector missing-candidate / cannot-build uses `GRAPH_PROJECTION_FAILED` (today it misuses `GRAPH_DIVERGENCE`). + +## File Changes + +| File | Action | Description | +| --- | --- | --- | +| `schemas/kernel/assessment/v1.schema.json` | Create | Closed `assessment/v1`; no `verdict`. | +| `schemas/kernel/assessment/fixtures/valid/v1-complete.json` | Create | Full binding fixture. | +| `schemas/kernel/assessment/fixtures/valid/v1-four-roles.json` | Create | Four payloads, one `evidence_id`, distinct roles. | +| `schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json` | Create | Missing required fields. | +| `schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json` | Create | `verdict` rejected. | +| `schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json` | Create | Assessment must not validate as `evidence/v2`. | +| `schemas/kernel/manifest.json` | Modify | Register family `assessment`. | +| `schemas/kernel/contract-claims.json` | Modify | Additive claims; do not replace v2 claims. | +| `schemas/kernel/assurance-graph/v1.schema.json` | Modify | Optional persistable `canonical_inputs`. Do **not** touch `evidence/v2`, `verification/v2`, K1 v1. | +| `scripts/lib/independent-verifier/collector-provenance.js` | Create | Allowlist mapper. | +| `scripts/lib/independent-verifier/assessment.js` | Create | `computeAssessmentId`, emit/validate. | +| `scripts/lib/independent-verifier/obligation-coverage.js` | Create | MUST walk, deferral, reason codes. | +| `scripts/lib/independent-verifier/index.js` | Modify | Order, fail-closed projection, return `assessments`. | +| `scripts/lib/independent-verifier/evidence.js` | Modify | Derive class from collector; keep `computeEvidenceId` observation-only. | +| `scripts/lib/assurance-graph/projector.js` | Modify | Canonical fingerprint; satisfies from assessments; typed `rejectForbidden`; export it. | +| `scripts/lib/assurance-graph/index.js` | Modify | Replay from persistable outputs. | +| `scripts/lib/k6b-schema-fixtures.test.js` | Modify | Assessment fixtures, pin freeze, four-role distinctness. | +| `scripts/lib/independent-verifier/index.test.js` | Modify | Adversarial MUST / collector / facade cases; add collector on existing harness evidence. | +| `scripts/lib/assurance-graph/index.test.js` | Modify | `graph_id` inputs, replay, substring-id, structured kind. | +| `scripts/k6b-verifier-assurance-graph-e2e.test.js` | Modify | Persist assessments + canonical_inputs; replay equality; contract/policy churn. | +| `docs/roadmaps/harness-evolution.md` | Modify | K6b `revise`; K6c `blocked-by-K6b-remediation` during apply. | +| `docs/architecture/harness-evolution.md` | Modify | Same status flip (no runtime module). | + +Archive of this change (not apply) restores K6b `done` and K6c `next-eligible`. + +## Interfaces / Contracts + +```javascript +// $id: ospec://schemas/kernel/assessment/v1 +{ + schema_version: 1, kind: "assessment/v1", + assessment_id, // sha256Fingerprint("assessment/v1", { schema_version, kind, evidence_id, role, + // obligation_id, node_id, candidate_id, policy_snapshot_id }) + evidence_id, role, obligation_id, node_id, candidate_id, policy_snapshot_id +} // additionalProperties: false; no verdict + +verifyCandidate({ ..., rawEvidence: [{ role, bytes, origin, node_id, obligation_id, + collector: { id, transport }, provenance /* claim only */ }] }) +// -> { ok, strategy, evidence, assessments, verification, assurance_graph } +// unique-sort(verification.evidence_ids) may list one E; assessments.length may be 4 + +projectAssuranceGraph({ canonicalInputs, candidate, executionGraph, evidence, assessments, verification }) +``` + +Raw `collector` is harness metadata, never copied onto `evidence/v2`. + +## Requirement Allocation + +| MUST scenario | Allocation | +| --- | --- | +| MUST without evidence / unknown `obligation_id` / wrong node | `obligation-coverage.js` | +| Four roles → four assessments; unique-sort still one E | `assessment.js` + `verdict.js` (unchanged unique-sort) | +| Facade fail-closed without graph | `independent-verifier/index.js` | +| Collector-derived strong class; payload-only strong fails; model-reported insufficient; stale/foreign/fabricated | `collector-provenance.js` + `evidence.js` | +| PASS requires strategy **and** MUST coverage | `index.js` after both gates | +| Evidence with `verdict` rejected | existing `evidence.js` / schema (unchanged) | +| `rejectForbidden` by kind/namespace; `REQ-add-authorization-header` valid | `projector.js` | +| Replay from persistable outputs; `satisfies` from assessments | `assurance-graph/index.js` + projector | +| Canonical inputs in `graph_id`; contract/policy change → divergence | `projector.js` `sha256Fingerprint` preimage | +| Matching inputs project; forbidden relations | existing projector + typed reject | +| Assessment fixtures, cross-family, four-role ids, frozen v2/K1 pins | `assessment/v1.schema.json` + `k6b-schema-fixtures.test.js` | +| Inventory includes assessment without mutating K6b v2 pins | `manifest.json` / `contract-claims.json` | +| Roadmap K6b revise / K6c blocked | both `harness-evolution.md` files | + +## Testing Strategy + +`testing.tdd_mode` stays `focused`. Each apply task is RED → GREEN → triangulate (adversarial) → REFACTOR. + +| Layer | What to Test | Approach | +| --- | --- | --- | +| Unit | MUST walk, deferral, unknown id, wrong node | Graph with extra MUST and no assessment; deferred skip; alien `obligation_id`. | +| Unit | Four-role assessments | Same bytes/node/provenance, four roles → one `evidence_id`, four `assessment_id`. | +| Unit | Collector allowlist | Payload `runtime-observed` without collector fails; allowlisted `node-test` passes. | +| Unit | `rejectForbidden` | `REQ-add-authorization-header` + `kind: requirement` ok; `kind: authorization` fails. | +| Contract | `assessment/v1` | Valid complete; missing fields; `verdict`; cross-family vs evidence/verification; byte-identical `evidence/v2`, `verification/v2`, K1 pins. | +| Unit | `graph_id` preimage | Flip contract, policy, execution-graph, or OpenSpec digest → new id; order permutation unchanged. | +| Integration | Facade | Stub projector failure → `ok: false`, `GRAPH_PROJECTION_FAILED`, no PASS. | +| E2E | Replay | Persist assessments + `canonical_inputs`; replay byte-identical; C1→C2 / P1→P2 → `GRAPH_DIVERGENCE`. | + +## Migration / Rollout + +No data migration. Additive schema + runtime gates. Existing verifier tests must supply `collector` on raw evidence that claims a strong class. `graph_id` values change (preimage expansion); static schema fixtures stay valid (pattern-only). Rollback reverts this change as a unit; v2 and K1 pins never move. + +## Open Questions + +None. `sdd-propose-001` is resolved as `ospec://schemas/kernel/assessment/v1`. diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/prepared-specs/assurance-graph/spec.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/prepared-specs/assurance-graph/spec.md new file mode 100644 index 00000000..eb39af97 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/prepared-specs/assurance-graph/spec.md @@ -0,0 +1,168 @@ +# 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: 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: 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: 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: 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: 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. + +#### 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 +- 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 diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/prepared-specs/independent-verification/spec.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/prepared-specs/independent-verification/spec.md new file mode 100644 index 00000000..2399d93f --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/prepared-specs/independent-verification/spec.md @@ -0,0 +1,228 @@ +# 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: 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: 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: 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: 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 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 +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. + +#### 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 + +### 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 + +- 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 +- 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 + +### 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` diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/prepared-specs/kernel-contract-schemas/spec.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/prepared-specs/kernel-contract-schemas/spec.md new file mode 100644 index 00000000..6129af8a --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/prepared-specs/kernel-contract-schemas/spec.md @@ -0,0 +1,847 @@ +# 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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}$`), 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. + +#### Scenario: Valid assessment fixture passes + +- GIVEN a complete assessment/binding payload with role, obligation_id, node_id, evidence_id, and policy-snapshot identity +- 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: 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 +- THEN those schema and fixture bytes and K1 pins MUST remain byte-identical diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/proposal.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/proposal.md new file mode 100644 index 00000000..099b3dd8 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/proposal.md @@ -0,0 +1,74 @@ +# Proposal: k6b-verification-integrity-remediation + +## Intent + +Cerrar seis defectos verificados del verifier y Assurance Graph de K6b (v2.50.0) sin reabrir el macro-slice ni iniciar K6c–K8. `evaluateStrategy()` solo comprueba roles: **strategy satisfied ≠ Execution Graph satisfied**. PASS exige evidencia admisible por cada MUST, bound al nodo correcto, con binding persistible y proyección fail-closed. + +## Scope + +### In Scope +- Walk MUST / Obligation Manifest / `required_evidence`; PASS solo con evidencia admisible en el `node_id` correcto. +- Binding aditivo persistible (assessment o verification evolucionada); no mutar `evidence/v2` ni K1 v1. +- Provenance fuerte derivada del collector/transport (digest ≠ origen; sin PKI). +- `graph_id` fingerprinta contract digest, policy snapshot, execution-graph digest e input OpenSpec; replay persistido. +- Proyección fallida ⇒ `GRAPH_DIVERGENCE` / `GRAPH_PROJECTION_FAILED`; facade no retorna `ok: true` sin grafo. +- `rejectForbidden` por kind/namespace, no substring del id. +- Tests adversariales (MUST sin evidencia, obligation_id inexistente, nodo incorrecto, un EvidenceId en cuatro roles, provenance reclasificada, replay, cambio de contract/policy). +- Roadmap: K6b `revise`; K6c `blocked-by-K6b-remediation` hasta archive; luego K6b `done`, K6c `next-eligible`. + +### Out of Scope +- Macro-slice K6b; K6c/K6d/K7/K8; PKI; mutar `evidence/v2` o K1 v1; cambiar el compilador K4a. + +## Capabilities + +### New Capabilities +- None. + +### Modified Capabilities +- `independent-verification`: cobertura MUST, assessment persistible, provenance de collector, proyección fail-closed. +- `assurance-graph`: fingerprint de canonicalInputs, replay persistido, `rejectForbidden` por kind. +- `kernel-contract-schemas`: familia aditiva de assessment/binding; pins `evidence/v2`, `verification/v2` y K1 v1 intactos. + +## Approach + +Tras strategy, evaluar MUST no diferidas con assessments persistibles (role + obligation + node + policy). `evidence/v2` permanece observación; el assessment id evita colapsar cuatro roles. Mapear collector/transport a clase fuerte; payload sin canal de confianza falla cerrado. Incluir canonicalInputs en `graph_id`. Si `projectAssuranceGraph` falla, el facade falla cerrado. Docs: K6b `revise`, K6c blocked. + +## Affected Areas + +| Area | Impact | Description | +|------|--------|-------------| +| `scripts/lib/independent-verifier/` | Modified | Walk MUST, binding, provenance, fail-closed | +| `scripts/lib/assurance-graph/projector.js` | Modified | canonicalInputs; kind/namespace | +| `schemas/kernel/` | New (additive) | Assessment/binding + fixtures | +| `scripts/k6b-verifier-assurance-graph-e2e.test.js` | Modified | Replay persistido | +| `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 | +|------|------------|------------| +| Consumers leen solo `evidence/v2` | High | Schema aditivo + E2E persistido | +| Unique-sort oculta roles | High | Assessment id incluye role+obligation | +| Collector mal mapeado | Med | Allowlist fail-closed | +| 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 + +- K6b archivado (`2026-08-27-k6b-verifier-evidence-assurance-graph`); Obligation Manifest K4a; schemas `evidence/v2`, `verification/v2`, `assurance-graph/v1`, `execution-graph/v1`. Delivery: `exception-ok`. + +## Success Criteria + +- [ ] MUST sin evidencia, `obligation_id` inexistente o nodo incorrecto ⇒ FAIL cerrado. +- [ ] Cuatro roles sobre la misma observación producen assessments distintos. +- [ ] Provenance fuerte no se acepta solo desde el payload. +- [ ] Replay desde salidas persistidas; cambio de contract/policy cambia digest y reconcilia. +- [ ] Proyección fallida ⇒ `ok: false` (`GRAPH_DIVERGENCE` o `GRAPH_PROJECTION_FAILED`). +- [ ] `REQ-add-authorization-header` no se rechaza por substring. +- [ ] Roadmap: K6b `revise` y 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-verification-integrity-remediation/specs/assurance-graph/spec.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/specs/assurance-graph/spec.md new file mode 100644 index 00000000..f0c0721f --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/specs/assurance-graph/spec.md @@ -0,0 +1,119 @@ +# Delta for Assurance Graph + +## ADDED Requirements + +### 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. + +#### 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 +- 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 + +## MODIFIED Requirements + +### 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: 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` diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/specs/independent-verification/spec.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/specs/independent-verification/spec.md new file mode 100644 index 00000000..7b907a58 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/specs/independent-verification/spec.md @@ -0,0 +1,158 @@ +# Delta for Independent Verification + +## ADDED 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 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 +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. + +#### 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 + +### 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 + +- 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 +- 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 + +### 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` + +## MODIFIED Requirements + +### 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: 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 diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md new file mode 100644 index 00000000..a6f2d774 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md @@ -0,0 +1,120 @@ +# Delta for kernel-contract-schemas + +## ADDED 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}$`), 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. + +#### Scenario: Valid assessment fixture passes + +- GIVEN a complete assessment/binding payload with role, obligation_id, node_id, evidence_id, and policy-snapshot identity +- 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: 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 +- THEN those schema and fixture bytes and K1 pins MUST remain byte-identical + +## MODIFIED Requirements + +### 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 diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/state.yaml b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/state.yaml new file mode 100644 index 00000000..8f52dc1d --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/state.yaml @@ -0,0 +1,170 @@ +change: "k6b-verification-integrity-remediation" +status: "archived" +last_updated: "2026-08-27T19:12:00Z" +classification: high-risk +blocking_questions: [] + +owner: + author: Manuel Retamozo + branch: feat/k6b-verifier-evidence-assurance-graph + +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-27T13:33:00Z" + synthesis: "Remediación focal k6b-verification-integrity-remediation sobre el verifier y el Assurance Graph de K6b, sin reabrir el macro-slice ni tocar K6c/K6d/K7/K8. Un PASS exigirá evidencia admisible para cada obligación MUST del Execution Graph; el binding EvidenceId ↔ role ↔ obligation_id ↔ node_id ↔ policy será persistible y versionado (sin mutar evidence/v2 in-place); la provenance fuerte saldrá del collector/canal, no del payload; la proyección fallará cerrada y su fingerprint ligará los inputs canónicos. El roadmap marcará K6b revise y K6c bloqueado hasta ese cierre." + scope: "In: cobertura de obligaciones MUST del Execution Graph; binding persistible EvidenceId↔role↔obligation↔node↔policy (contrato versionado aditivo); provenance fuerte derivada del collector; fingerprint canónico del Assurance Graph; fail-closed si falla la proyección; tests adversariales; actualización de roadmap K6b revise / K6c blocked-by-remediation. Out: K6c challenges, K7 review, K8 attestation, PKI/criptografía de provenance, mutación in-place de evidence/v2 o contratos K1." + applies_to: + - change-classification + - id: execution-mode-001 + gate: execution-mode + decision: auto + source: vscode/askQuestions + accepted_at: "2026-08-27T13:39: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-27T13:39:30Z" + applies_to: + - sdd-tasks + - sdd-apply + - id: 4r-remediation-001 + gate: review-workload + decision: remediate-criticals-and-warnings + source: vscode/askQuestions + accepted_at: "2026-08-27T15:00:00Z" + applies_to: + - sdd-apply + - review-correction + - id: k6b-verification-integrity-remediation-bounded-review-001 + gate: 4r-review-gate + decision: new-candidate + source: vscode/askQuestions + accepted_at: "2026-08-27T18:45:00Z" + reason: "Successor new-candidate: migrate harness collector off worker envelopes, green the verifier suite, and close remaining advisory WARNINGs that fit." + applies_to: + - sdd-design + - sdd-tasks + - sdd-apply + - sdd-verify + +baseline_fingerprints: + independent-verification: "sha256:8063703bb9860b0fbff8b49894fa17932c222cb58f1dea028bda57a00330fc6d" + assurance-graph: "sha256:c7d171aab463f80f504aff6706e7847ef44a849ae79a8e06f13a8e4e17a1795d" + kernel-contract-schemas: "sha256:fabcbc4edb0ab2934414ca9bf5bca0f88c7e5a3759a0c427f739d0475835d25d" + +assumptions: + - id: sdd-propose-001 + phase: sdd-propose + statement: "The persistable binding is an additive assessment/binding family; the exact schema $id is left to sdd-design." + reversibility: high + basis: "Accepted intent-briefing forbids in-place evidence/v2 mutation and allows an additive assessment structure or verification-contract evolution; pinning $id in the proposal would pre-empt design." + recorded_at: "2026-08-27T13:43:36Z" + status: resolved + resolved_by: sdd-design + resolved_at: "2026-08-27T14:05:00Z" + resolution: "ospec://schemas/kernel/assessment/v1 (kind assessment/v1; manifest key assessment; ADR-001)" + +phases: + proposal: + status: done + artifact: "openspec/changes/k6b-verification-integrity-remediation/proposal.md" + summary: "Cierra 6 defectos K6b: cobertura MUST, binding persistible, provenance de collector, fingerprint canónico, proyección fail-closed y rejectForbidden por kind." + key_decisions: + - "Familia aditiva de assessment/binding; no mutar evidence/v2 ni K1 v1" + - "PASS exige evidencia admisible por cada MUST bound al nodo correcto" + - "Roadmap: K6b revise; K6c blocked-by-K6b-remediation hasta archive" + spec: + status: done + artifacts: + - "openspec/changes/k6b-verification-integrity-remediation/specs/independent-verification/spec.md" + - "openspec/changes/k6b-verification-integrity-remediation/specs/assurance-graph/spec.md" + - "openspec/changes/k6b-verification-integrity-remediation/specs/kernel-contract-schemas/spec.md" + summary: "Deltas: MUST coverage, persistable assessments, collector provenance, canonical graph_id, fail-closed projection, kind/namespace rejectForbidden." + key_decisions: + - "PASS requires MUST obligation coverage bound to implementing nodes; strategy shape is insufficient" + - "Additive assessment identity includes role+obligation_id; evidence/v2 unchanged" + - "graph_id fingerprints canonicalInputs; rejectForbidden by kind/namespace; facade fail-closed" + clarify: + status: skipped + design: + status: done + artifact: "openspec/changes/k6b-verification-integrity-remediation/design.md" + summary: "Additive assessment/v1; MUST walk after strategy; collector allowlist; canonical graph_id; facade fail-closed; kind/namespace rejectForbidden." + key_decisions: + - "Schema $id ospec://schemas/kernel/assessment/v1; evidence/v2 unchanged (ADR-001)" + - "evaluateStrategy then MUST walk; assessment_id includes role+obligation_id (ADR-002)" + - "graph_id fingerprints canonicalInputs; projection failure is GRAPH_PROJECTION_FAILED (ADR-004)" + tasks: + status: done + artifact: "openspec/changes/k6b-verification-integrity-remediation/tasks.md" + summary: "8 fases, 28 tareas: assessment/v1 schema, MUST walk, collector provenance, facade fail-closed, graph_id canónico, replay, adversariales, roadmap." + key_decisions: + - "size-exception aprobado (~900–1200 líneas); single PR lógico en 4 work units" + - "TDD RED→GREEN→TRIANGULATE por módulo; adversariales como tareas de primera clase" + - "Publicación assessment/v1 en Phase 1 (lección K6b V001)" + apply: + status: done + artifact: "openspec/changes/k6b-verification-integrity-remediation/apply-progress.md" + summary: "Phases 1–8 done: assessment/v1, MUST walk, collector allowlist, facade fail-closed, canonical graph_id, E2E replay, K6b revise." + key_decisions: + - "size:exception single batch; v2/K1 pins unchanged" + - "satisfies edges rebuild from persistable assessments only" + - "payload-only strong provenance is UNTRUSTED_COLLECTOR" + verify: + status: done + verdict: PASS + artifact: "openspec/changes/k6b-verification-integrity-remediation/verify-report.md" + summary: "Re-verify PASS after 4R successor: 30/30 tasks; 35/35 MUST; npm test 2754 pass, 0 fail; focal 75/75." + key_decisions: + - "verify_lineage absent → full discovery; 4R advisories not escalated" + - "quality_gates policy absent; no known-issues write (no CRITICAL/WARNING)" + archive: + status: done + artifact: "openspec/changes/k6b-verification-integrity-remediation/archive-report.md" + summary: "Plan-and-report emitido: 3 spec merges, 4 ADR promotions; runtime archive-transaction pendiente." + key_decisions: + - "prepared-specs/ bajo change-local; live openspec/specs y docs/adr son runtime-owned" + - "models.yaml excluido del candidato; 2 SUGGESTION verify y 4R WARNINGs no bloquean" + - "source_fingerprint sha256:6effe8dc40f89da6c44167ebfb0bab853fdfa5babdb12496fd1a7db82bd76234" + +gates: + 4r-review-gate: + status: done + schema_version: 1 + classification: high-risk + depth: + review: strict + lineage_status: approved + lineage_id: "sha256:a051818ce2bb310c5fa3a29c8a7b730a564dd5b44ff1da238a73089fcce94c02" + lineage_revision: 9 + lineage_artifact: openspec/changes/k6b-verification-integrity-remediation/.4r/lineage.json + predecessor_lineage_id: "sha256:262dda4ab0b3ec0fe60b7db34683c55d3c4d2590fe69282c40197cbc95aacf42" + findings_summary: "0 BLOCKER, 0 CRITICAL, 6 WARNING, 1 SUGGESTION" + archive_allowed: true + selected_specialists: [risk, reliability, resilience, readability] + evidence: + schema_version: 1 + fingerprint: "sha256:66b1b1c10af6adfd036aa014032450b530160d81f05f9dae3af5c1cf09ade779" + generalist: + status: needs-specialist + specialists: [risk, reliability] + reason: "signals=design-risk,diff-auth-permission,metadata-runtime;dimensions=risk,reliability" diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/tasks.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/tasks.md new file mode 100644 index 00000000..5bf74938 --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/tasks.md @@ -0,0 +1,122 @@ +# Tasks: k6b-verification-integrity-remediation + +## Spec/Design Reconciliation + +| Requirement / Scenario | Priority | Design Allocation | Status | Notes | +|------------------------|----------|-------------------|--------|-------| +| REQ-independent-verification-005: MUST without evidence fails closed | MUST | `obligation-coverage.js` | covered-by-design | `UNFULFILLED_MUST` after strategy | +| REQ-independent-verification-005: Nonexistent obligation_id fails closed | MUST | `obligation-coverage.js` | covered-by-design | `UNKNOWN_OBLIGATION_ID` | +| REQ-independent-verification-005: Wrong implementing node fails closed | MUST | `obligation-coverage.js` | covered-by-design | `WRONG_IMPLEMENTING_NODE` | +| REQ-independent-verification-006: Four roles → four assessments | MUST | `assessment.js` + `verdict.js` | covered-by-design | Assessment id includes role+obligation | +| REQ-independent-verification-007: Failed projection → ok:false | MUST | `independent-verifier/index.js` | covered-by-design | No PASS without graph | +| REQ-independent-verification-003: Collector-derived runtime-observed | MUST | `collector-provenance.js`, `evidence.js` | covered-by-design | Allowlist fail-closed | +| REQ-independent-verification-003: Model-reported insufficient | MUST | `collector-provenance.js`, `evidence.js` | covered-by-design | `INSUFFICIENT_PROVENANCE` | +| REQ-independent-verification-003: Stale/foreign/fabricated rejected | MUST | `evidence.js` (existing) | covered-by-design | Unchanged gates | +| REQ-independent-verification-003: Payload-only strong fails closed | MUST | `collector-provenance.js` | covered-by-design | `UNTRUSTED_COLLECTOR` | +| REQ-independent-verification-004: PASS requires strategy + MUST coverage | MUST | `index.js`, `verdict.js` | covered-by-design | Both gates required | +| REQ-independent-verification-004: Evidence carrying verdict rejected | MUST | `evidence.js`, schemas (unchanged) | covered-by-design | Existing v2 guard | +| REQ-assurance-graph-005: REQ-add-authorization-header valid | MUST | `projector.js` `rejectForbidden` | covered-by-design | Kind/namespace only | +| REQ-assurance-graph-005: Structured authorization kind rejected | MUST | `projector.js` | covered-by-design | Forbidden kind/namespace | +| REQ-assurance-graph-006: Replay byte-identical from persistable outputs | MUST | `assurance-graph/index.js` | covered-by-design | No ephemeral obligation_ids | +| REQ-assurance-graph-001: Matching inputs project graph | MUST | `projector.js` | covered-by-design | canonicalInputs in preimage | +| REQ-assurance-graph-001: Divergent graph fails closed | MUST | `assurance-graph/index.js` | covered-by-design | `GRAPH_DIVERGENCE` | +| REQ-assurance-graph-001: Contract/policy change → divergence | MUST | `projector.js`, reconcile | covered-by-design | C1→C2 / P1→P2 | +| REQ-assurance-graph-002: Same inputs same digest | MUST | `projector.js` | covered-by-design | Sorted canonical sets | +| REQ-assurance-graph-002: Forbidden relations rejected | MUST | `projector.js`, schema | covered-by-design | Existing + typed reject | +| REQ-assurance-graph-002: Canonical input change → distinct graph_id | MUST | `projector.js` | covered-by-design | contract/policy/exec/openspec digests | +| REQ-kernel-contract-schemas-027: Valid assessment fixture passes | MUST | `assessment/v1.schema.json`, fixtures | covered-by-design | ADR-001 $id | +| REQ-kernel-contract-schemas-027: Cross-family + verdict fail closed | MUST | fixtures + `k6b-schema-fixtures.test.js` | covered-by-design | No evidence/verification alias | +| REQ-kernel-contract-schemas-027: Four-role distinct identities | MUST | `assessment.js`, fixtures | covered-by-design | `v1-four-roles.json` | +| REQ-kernel-contract-schemas-027: v2/K1 pins frozen | MUST | `k6b-schema-fixtures.test.js` | covered-by-design | Byte-identical assert | +| REQ-kernel-contract-schemas-001: Assessment in inventory | MUST | `manifest.json`, `contract-claims.json` | covered-by-design | Additive registration | + +### Reconciliation Verdict + +- MUST coverage: complete (12 REQs, 24/24 MUST scenarios covered-by-design). +- SHOULD/MAY gaps: none. +- Ambiguities to track: none. + +## Review Workload Forecast + +| Field | Value | +|-------|-------| +| Estimated changed lines | ~900–1200 líneas | +| 400-line budget risk | High | +| Chained PRs recommended | Yes | +| Suggested split | Single PR con `size:exception`; orden: assessment/v1 schema → verifier modules → assurance-graph → adversarial/E2E → roadmap docs | +| 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 | Publicar `assessment/v1` schema, fixtures y registro aditivo | PR 1 (single) | manifest + contract-claims; v2/K1 pins byte-identical | +| 2 | Módulos verifier: assessment, collector-provenance, obligation-coverage, facade | PR 1 (single) | Strategy → MUST walk → project fail-closed | +| 3 | Assurance Graph: canonical graph_id, rejectForbidden, replay | PR 1 (single) | satisfies desde assessments persistibles | +| 4 | Tests adversariales, E2E replay y roadmap K6b revise / K6c blocked | PR 1 (single) | `k6b-verifier-assurance-graph-e2e.test.js` + harness-evolution docs | + +### Checklist Status Legend + +- `[ ]` Not implemented yet +- `[~]` Implemented but not yet verified locally +- `[x]` Implemented and verified locally + +## Phase 1: Assessment/v1 Schema Publication + +- [x] 1.1 RED: Extender `scripts/lib/k6b-schema-fixtures.test.js` con tests que exigen familia `assessment` en `manifest.json`/`contract-claims.json`, fixtures valid/invalid, cross-family rejection y assert byte-identical de `evidence/v2`, `verification/v2` y pins K1. [REQ-kernel-contract-schemas-027, REQ-kernel-contract-schemas-001] +- [x] 1.2 GREEN: Crear `schemas/kernel/assessment/v1.schema.json` (`$id: ospec://schemas/kernel/assessment/v1`, `kind: assessment/v1`, `additionalProperties: false`, sin `verdict`). [REQ-kernel-contract-schemas-027] +- [x] 1.3 GREEN: Crear fixtures `schemas/kernel/assessment/fixtures/valid/v1-complete.json`, `v1-four-roles.json` e invalid `v1-missing-required.json`, `v1-with-verdict.json`, `v1-evidence-alias.json`. [REQ-kernel-contract-schemas-027] +- [x] 1.4 GREEN: Registrar familia `assessment` en `schemas/kernel/manifest.json` y claims aditivos en `schemas/kernel/contract-claims.json`; ejecutar `node --test scripts/lib/k6b-schema-fixtures.test.js`. [REQ-kernel-contract-schemas-001] +- [x] 1.5 TRIANGULATE: Assert cuatro roles comparten un `evidence_id` pero producen cuatro `assessment_id` distintos en fixtures + test. [REQ-kernel-contract-schemas-027] + +## Phase 2: Assessment Identity Module + +- [x] 2.1 RED: Crear `scripts/lib/independent-verifier/assessment.test.js` con tests para `computeAssessmentId`, emisión/validación y rechazo de payloads con `verdict`. [REQ-independent-verification-006] +- [x] 2.2 GREEN: Implementar `scripts/lib/independent-verifier/assessment.js` (`computeAssessmentId`, `emitAssessment`, validate contra schema). [REQ-independent-verification-006] +- [x] 2.3 TRIANGULATE: Mismo bytes/provenance/node, cuatro roles → un `evidence_id`, cuatro `assessment_id` distintos. [REQ-independent-verification-006] + +## Phase 3: Collector Provenance Allowlist + +- [x] 3.1 RED: Tests en `scripts/lib/independent-verifier/index.test.js` para payload `runtime-observed` sin collector → `UNTRUSTED_COLLECTOR`; allowlisted `node-test`+`tool-execution-transport` → `runtime-observed`; worker → `model-reported` insuficiente para obligación runtime. [REQ-independent-verification-003] +- [x] 3.2 GREEN: Crear `scripts/lib/independent-verifier/collector-provenance.js` con tabla allowlist fail-closed (ADR-003). [REQ-independent-verification-003] +- [x] 3.3 GREEN: Modificar `scripts/lib/independent-verifier/evidence.js` para derivar clase desde collector/transport; mantener `computeEvidenceId` observation-only. [REQ-independent-verification-003] +- [x] 3.4 TRIANGULATE: Provenance reclasificada (payload strong vs collector weak) falla cerrado; digest ≠ origen. [REQ-independent-verification-003] + +## Phase 4: Obligation Manifest MUST Walk + +- [x] 4.1 RED: Tests en `scripts/lib/independent-verifier/obligation-coverage.test.js` (o `index.test.js`): MUST sin evidencia → `UNFULFILLED_MUST`; `obligation_id` alien → `UNKNOWN_OBLIGATION_ID`; nodo B ∉ `implemented_by` → `WRONG_IMPLEMENTING_NODE`; deferral aprobado skip. [REQ-independent-verification-005] +- [x] 4.2 GREEN: Implementar `scripts/lib/independent-verifier/obligation-coverage.js` con walk post-strategy, deferral predicate K4a y emisión de assessments persistibles. [REQ-independent-verification-005] +- [x] 4.3 TRIANGULATE: Strategy satisfecha pero MUST sin binding → FAIL; empty `required_evidence` en MUST no-deferred → FAIL. [REQ-independent-verification-005] + +## Phase 5: Verifier Facade Integration + +- [x] 5.1 RED: Tests en `index.test.js`: orden bindings→strategy→normalize→MUST→project; stub projector failure → `ok: false`, `GRAPH_PROJECTION_FAILED`, sin `assurance_graph` ni PASS; strategy fail short-circuits sin upgrade MUST. [REQ-independent-verification-007, REQ-independent-verification-004] +- [x] 5.2 GREEN: Modificar `scripts/lib/independent-verifier/index.js`: integrar MUST walk, retornar `assessments`, fail-closed si proyección falla; PASS solo con strategy + MUST coverage. [REQ-independent-verification-004, REQ-independent-verification-007] +- [x] 5.3 GREEN: Actualizar tests existentes en `index.test.js` para incluir `collector` en raw evidence que reclama clase fuerte. [REQ-independent-verification-003] +- [x] 5.4 REFACTOR: Verificar `verification.evidence_ids` unique-sort sigue listando un E con cuatro assessments distintos. [REQ-independent-verification-006, REQ-independent-verification-004] + +## Phase 6: Assurance Graph Remediation + +- [x] 6.1 RED: Tests en `scripts/lib/assurance-graph/index.test.js`: `graph_id` cambia al alterar contract/policy/execution-graph/openspec digest; permutación nodos/edges no cambia digest; `REQ-add-authorization-header` + `kind: requirement` válido; `kind: authorization` rechazado. [REQ-assurance-graph-002, REQ-assurance-graph-005] +- [x] 6.2 GREEN: Modificar `scripts/lib/assurance-graph/projector.js`: preimage canonicalInputs en `graph_id`; `satisfies` desde assessments; exportar `rejectForbidden` por kind/namespace; `GRAPH_PROJECTION_FAILED` para missing-candidate. [REQ-assurance-graph-002, REQ-assurance-graph-005] +- [x] 6.3 GREEN: Modificar `schemas/kernel/assurance-graph/v1.schema.json` con campo opcional `canonical_inputs` persistible. [REQ-assurance-graph-002] +- [x] 6.4 RED: Tests replay: recomputar desde assessments+evidence+verification+canonical_inputs sin ephemeral `obligation_ids` → byte-identical; C1→C2 / P1→P2 → `GRAPH_DIVERGENCE`. [REQ-assurance-graph-006, REQ-assurance-graph-001] +- [x] 6.5 GREEN: Modificar `scripts/lib/assurance-graph/index.js` para replay desde salidas persistibles y reconciliación fail-closed. [REQ-assurance-graph-006] + +## Phase 7: Adversarial Tests and E2E + +- [x] 7.1 RED: Extender `scripts/k6b-verifier-assurance-graph-e2e.test.js` con persistencia de assessments + `canonical_inputs`, replay equality y contract/policy churn → `GRAPH_DIVERGENCE`. [REQ-assurance-graph-006, REQ-assurance-graph-001] +- [x] 7.2 GREEN: Implementar flujo E2E completo con assessments persistidos y replay byte-identical. [REQ-assurance-graph-006] +- [x] 7.3 TRIANGULATE adversariales en `index.test.js`: un EvidenceId en cuatro roles; MUST sin evidencia; obligation_id inexistente; nodo incorrecto; proyección fallida. [REQ-independent-verification-005, REQ-independent-verification-006, REQ-independent-verification-007] +- [x] 7.4 Ejecutar suites focalizadas (`k6b-schema-fixtures.test.js`, `independent-verifier/*.test.js`, `assurance-graph/index.test.js`, `k6b-verifier-assurance-graph-e2e.test.js`) y registrar evidencia TDD en `apply-progress.md`. + +## Phase 8: Roadmap Documentation + +- [x] 8.1 Actualizar `docs/roadmaps/harness-evolution.md`: K6b `revise`; K6c `blocked-by-K6b-remediation` hasta archive de este change. [REQ-independent-verification-005 via proposal success criteria] +- [x] 8.2 Actualizar `docs/architecture/harness-evolution.md` con el mismo flip de estado (sin módulos runtime). [REQ-independent-verification-005 via proposal success criteria] diff --git a/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/verify-report.md b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/verify-report.md new file mode 100644 index 00000000..051864ee --- /dev/null +++ b/openspec/changes/archive/2026-08-27-k6b-verification-integrity-remediation/verify-report.md @@ -0,0 +1,199 @@ +## Verification Report + +**Change**: k6b-verification-integrity-remediation +**Version**: N/A (delta over archived K6b v2.50.0) +**Mode**: Strict TDD (orchestrator-active; `openspec/config.yaml` remains `testing.tdd_mode: focused`; apply recorded focused cycles) +**Re-verify**: full discovery (`verify_lineage` absent). Successor 4R lineage `sha256:a051818ce2bb310c5fa3a29c8a7b730a564dd5b44ff1da238a73089fcce94c02` is approved (0 BLOCKER, 0 CRITICAL). Predecessor `sha256:262dda4ab0b3ec0fe60b7db34683c55d3c4d2590fe69282c40197cbc95aacf42` also approved. Candidate excludes `models.yaml` (dirty before this change). + +### Completeness +| Metric | Value | +|--------|-------| +| Tasks total | 30 | +| Tasks complete | 30 | +| Tasks incomplete | 0 | + +All Phase 1–8 items in `tasks.md` are `[x]`. `apply-progress.md` records batches 1–5 complete (size:exception apply plus 4R slices and successor `new-candidate` helpers). + +Lineage router: `verify_lineage` absent → `run-discovery`. Assumption `sdd-propose-001` is already `resolved` (design `$id`); Step 2b was a no-op. `quality_gates:` is commented out in `openspec/config.yaml` → Step 9a skipped. 4R advisory WARNINGs are not treated as verify CRITICALs: the covering runtime tests pass the MUST acceptance criteria. + +### Build & Tests Execution +**Build**: ✅ Passed (no dedicated `build_command`; `npm test` → `node scripts/check.js` generated and validated targets; ended with `All checks passed.`) +```text +npm test +==> Native Node tests +ℹ tests 2756 +ℹ pass 2754 +ℹ fail 0 +ℹ skipped 2 +ℹ duration_ms 62877.8675 +==> Generate + validate (claude generation-only; other targets validated) +All checks passed. +exit 0 +``` + +**Tests**: ✅ 2754 passed / ❌ 0 failed / ⚠️ 2 skipped +```text +Command: npm test (node scripts/check.js) +Native runner: node --test scripts/**/*.test.js +Skipped tests are unrelated Windows lock-contention / platform probes (ospec-state.test.js), not this change. + +Focal suites (this re-verify): +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 \ + scripts/lib/k2a-maturity-docs.test.js +→ 75 pass, 0 fail, 0 skipped +``` + +**Manual verification**: not performed +```text +N/A — automated runtime evidence covers all MUST scenarios. +``` + +**Coverage**: ➖ Not available / threshold: 0% → ➖ Not available (`testing.coverage.available: false`) + +### Spec Compliance Matrix +| Requirement | Scenario | Evidence Level | Source | Result | Notes | +|-------------|----------|----------------|--------|--------|-------| +| REQ-independent-verification-005 | MUST without admissible evidence fails closed | `runtime-test` | `obligation-coverage.test.js` > UNFULFILLED_MUST; `index.test.js` after strategy | PASS | Identifies `req-repair-001`; no verification emitted | +| REQ-independent-verification-005 | Nonexistent obligation_id fails closed | `runtime-test` | `obligation-coverage.test.js`; `index.test.js` alien id | PASS | `UNKNOWN_OBLIGATION_ID` | +| REQ-independent-verification-005 | Evidence bound to the wrong implementing node fails closed | `runtime-test` | `obligation-coverage.test.js`; `index.test.js` other-node | PASS | `WRONG_IMPLEMENTING_NODE` | +| REQ-independent-verification-006 | Same EvidenceId used as four roles yields four assessments | `runtime-test` | `assessment.test.js`; `index.test.js` four-role facade | PASS | unique-sort `evidence_ids` size 1; four `assessment_id` | +| REQ-independent-verification-007 | Failed projection does not return ok without a graph | `runtime-test` | `index.test.js` projector stub | PASS | `GRAPH_PROJECTION_FAILED`; stub `GRAPH_DIVERGENCE`; no `assurance_graph`; no verification | +| REQ-independent-verification-003 | Runtime-observed evidence satisfies a test obligation | `runtime-test` | `index.test.js` allowlisted node-test collector | PASS | Stored provenance derived; collector not copied onto evidence/v2 | +| REQ-independent-verification-003 | Model-reported tests-passed is insufficient | `runtime-test` | `index.test.js` worker collector | PASS | Fail-closed (`INSUFFICIENT_PROVENANCE` or `UNTRUSTED_COLLECTOR`) | +| REQ-independent-verification-003 | Stale, foreign, or fabricated evidence is rejected | `runtime-test` | `index.test.js` FOREIGN_SUBJECT / FABRICATED_EVIDENCE / STALE_EVIDENCE | PASS | Unchanged gates retained; remint STALE covered | +| REQ-independent-verification-003 | Payload-claimed strong provenance without trusted collector fails closed | `runtime-test` | `index.test.js` UNTRUSTED_COLLECTOR; payload vs worker; envelope collector | PASS | Digest is not origin; harness `input.collector` only | +| REQ-independent-verification-004 | Sufficient evidence yields a verification verdict | `runtime-test` | `index.test.js` PASS and PASS WITH WARNINGS | PASS | Requires strategy + MUST walk + projection | +| REQ-independent-verification-004 | Evidence carrying verdict is rejected | `runtime-test` | `index.test.js` MIXED_EVIDENCE_VERDICT | PASS | | +| REQ-assurance-graph-005 | Requirement id containing authorization remains valid | `runtime-test` | `assurance-graph/index.test.js` rejectForbidden | PASS | `REQ-add-authorization-header` + `kind: requirement` | +| REQ-assurance-graph-005 | Structured authorization kind is rejected | `runtime-test` | `assurance-graph/index.test.js` kind + namespace | PASS | `FORBIDDEN_RELATION`; kind checked before allow-list | +| REQ-assurance-graph-006 | Replay from persisted outputs yields the same graph | `runtime-test` | `assurance-graph/index.test.js`; `k6b-verifier-assurance-graph-e2e.test.js` | PASS | Assessments + `canonical_inputs`; no ephemeral obligation_ids | +| REQ-assurance-graph-001 | Matching canonical inputs project a graph | `runtime-test` | `assurance-graph/index.test.js`; e2e double project | PASS | | +| REQ-assurance-graph-001 | Divergent graph fails closed | `runtime-test` | `assurance-graph/index.test.js` reconcile mutated graph_id/edges | PASS | `GRAPH_DIVERGENCE` | +| REQ-assurance-graph-001 | Contract or policy change forces reconciliation fail-closed | `runtime-test` | `assurance-graph/index.test.js`; e2e C1→C2 | PASS | Distinct `graph_id` then `GRAPH_DIVERGENCE` | +| REQ-assurance-graph-002 | Same inputs yield the same digest and edges | `runtime-test` | `assurance-graph/index.test.js` permutation; e2e | PASS | Sorted canonical sets | +| REQ-assurance-graph-002 | Forbidden later-slice relations are rejected | `runtime-test` | `k6b-schema-fixtures.test.js` reviewed-by; `rejectForbidden` | PASS | | +| REQ-assurance-graph-002 | Canonical input change yields a distinct graph_id | `runtime-test` | `assurance-graph/index.test.js` contract/policy/exec/openspec flips | PASS | Preimage includes all four digests + candidate + nodes/edges | +| REQ-kernel-contract-schemas-027 | Valid assessment fixture passes | `runtime-test` | `k6b-schema-fixtures.test.js` v1-complete.json | PASS | `$id` ospec://schemas/kernel/assessment/v1 | +| REQ-kernel-contract-schemas-027 | Cross-family substitution and verdict fail closed | `runtime-test` | `k6b-schema-fixtures.test.js` verdict + alias | PASS | additionalProperties: false | +| REQ-kernel-contract-schemas-027 | Four-role assessments remain distinct under the schema | `runtime-test` | `k6b-schema-fixtures.test.js` v1-four-roles.json | PASS | Array of four payloads; one evidence_id | +| REQ-kernel-contract-schemas-027 | Evidence v2, verification v2, and K1 v1 pins remain frozen | `static-proof` | `k6b-schema-fixtures.test.js` digest pins + `assertK1SchemasUnchanged` | PASS | Byte-identical assert (accepted for structural MUST) | +| REQ-kernel-contract-schemas-001 | Every required family has $id and version | `runtime-test` | `kernel-schema-fixtures.test.js` + later kN fixture suites + assessment registration | PASS | Assessment added to manifest | +| REQ-kernel-contract-schemas-001 | Consumer can pin a schema version | `runtime-test` | `loadSchemaById("ospec://schemas/kernel/assessment/v1")` | PASS | | +| REQ-kernel-contract-schemas-001 | K2.1 families are included | `runtime-test` | existing K2.1 schema fixture suite (npm test) | PASS | Unchanged by this delta | +| REQ-kernel-contract-schemas-001 | K2a families are included | `runtime-test` | existing K2a schema fixture suite (npm test) | PASS | Unchanged by this delta | +| REQ-kernel-contract-schemas-001 | k2a-1 transport envelope families are included | `runtime-test` | existing transport envelope fixture suite (npm test) | PASS | Unchanged by this delta | +| REQ-kernel-contract-schemas-001 | K3 execution identity families are included | `runtime-test` | `k3-schema-fixtures.test.js` (npm test) | PASS | Unchanged by this delta | +| REQ-kernel-contract-schemas-001 | K4a families are included | `runtime-test` | `k4a-schema-fixtures.test.js` (npm test) | PASS | Unchanged by this delta | +| REQ-kernel-contract-schemas-001 | K5 families are included | `runtime-test` | `k5-schema-fixtures.test.js` (npm test) | PASS | Unchanged by this delta | +| REQ-kernel-contract-schemas-001 | K6a families are included | `runtime-test` | existing K6a schema fixture suite (npm test) | PASS | Unchanged by this delta | +| REQ-kernel-contract-schemas-001 | K6b assurance-graph family is included | `runtime-test` | `k6b-schema-fixtures.test.js` | PASS | | +| REQ-kernel-contract-schemas-001 | Assessment/binding family is included without mutating K6b pins | `runtime-test` | `k6b-schema-fixtures.test.js` manifest + v2 digest pins | PASS | Additive registration | + +**Compliance summary**: 35/35 scenarios satisfied at acceptable evidence levels + +### TDD Compliance +| Check | Result | Details | +|-------|--------|---------| +| TDD Evidence reported | ✅ | Found in `apply-progress.md` (Batches 1–5, including 4R slices) | +| All tasks have tests | ✅ | 30/30 tasks mapped; docs 8.1–8.2 use `k2a-maturity-docs.test.js` | +| RED confirmed (tests exist) | ✅ | All listed test files exist on disk | +| GREEN confirmed (tests pass) | ✅ | Focal 75/75 pass; `npm test` 2754 pass, 0 fail | +| Triangulation adequate | ✅ | Multi-case per MUST (deferral variants, collector ids, graph_id flips, four roles) | +| Safety Net for modified files | ✅ | Safety-net column is descriptive (not N/A) for modified verifier/projector suites | + +**TDD Compliance**: 6/6 checks passed + +Apply recorded RED/GREEN as `[x]` (focused-cycle form) rather than `✅ Written` / `✅ Passed`. Test files exist and this re-verify executed them; the format difference is not treated as a protocol CRITICAL. + +--- + +### Test Layer Distribution +| Layer | Tests | Files | Tools | +|-------|-------|-------|-------| +| Unit | 14 | 2 | `node --test` (`assessment.test.js`, `obligation-coverage.test.js`) | +| Contract / docs | 11 | 2 | `node --test` (`k6b-schema-fixtures.test.js`, `k2a-maturity-docs.test.js`) | +| Integration | 49 | 2 | `node --test` (`independent-verifier/index.test.js`, `assurance-graph/index.test.js`) | +| In-process E2E | 1 | 1 | `node --test` (`k6b-verifier-assurance-graph-e2e.test.js`; no browser) | +| **Total (focal)** | **75** | **7** | | + +Capabilities list `e2e: false`. The K6b “e2e” file is in-process Node (verifier → project → replay), not Playwright/browser, so it does not use undetected e2e tools. + +--- + +### Changed File Coverage +Coverage analysis skipped — no coverage tool detected (`testing.coverage.available: false`). + +--- + +### Assertion Quality +**Assertion quality**: ✅ All assertions verify real behavior + +Scanned focal tests for tautologies, ghost loops over possibly-empty collections, type-only-only checks, zero-assertion cases, and tests that never call production code. Loops iterate fixtures or known non-empty arrays (four-role payloads, incomplete-deferral variants, collector id lists). Assertions check `reason_code`, identities, schema validity, and persistable graph equality. + +--- + +### Quality Metrics +**Linter**: ➖ Not available +**Type Checker**: ➖ Not available + +### Correctness (Static Evidence) +| Requirement | Status | Notes | +|------------|--------|-------| +| MUST walk after strategy | ✅ Implemented | `verifyCandidate` → `evaluateStrategy` then `walkMustObligations`; strategy failure short-circuits | +| Persistable assessment/v1 | ✅ Implemented | `assessment.js` fingerprints role+obligation; evidence/v2 has no role/obligation_id | +| Collector-derived provenance | ✅ Implemented | `collector-provenance.js` allowlist; envelope `collector` → `UNTRUSTED_COLLECTOR`; harness `input.collector` / `collectors[]` | +| Facade fail-closed projection | ✅ Implemented | `mapProjectionFailure`; no PASS / no graph on projector failure | +| Canonical `graph_id` | ✅ Implemented | Preimage: candidate_id, contract_digest, policy_snapshot_id, execution_graph_digest, openspec_input_digest, nodes, edges | +| `satisfies` from assessments | ✅ Implemented | Projector builds satisfies from persistable assessments only | +| `rejectForbidden` by kind/namespace | ✅ Implemented | FORBIDDEN_KINDS checked before allow-list; does not scan `id` substrings | +| Roadmap K6b revise / K6c blocked | ✅ Implemented | Both harness-evolution docs; `k2a-maturity-docs.test.js` | + +### Coherence (Design) +| Decision | Followed? | Notes | +|----------|-----------|-------| +| ADR-001 `ospec://schemas/kernel/assessment/v1` | ✅ Yes | Manifest key `assessment`; evidence/v2 and verification/v2 untouched | +| ADR-002 strategy then MUST; assessment id includes role+obligation | ✅ Yes | | +| ADR-003 collector/transport allowlist, no PKI | ✅ Yes | node-test/npm-test/node:test, tool-execution, host-adapter; envelope collector rejected | +| ADR-004 canonicalInputs in graph_id; missing candidate is GRAPH_PROJECTION_FAILED | ✅ Yes | Facade also maps projector `GRAPH_DIVERGENCE`; mismatched provided canonicalInputs fail closed | +| Companion K1 inventory carve-out for `schemas/kernel/assessment/` | ✅ Yes | Documented in apply-progress; assessment is additive, not a K1 family | +| `v1-four-roles.json` as array of four payloads | ✅ Yes | k1-schema-compat skips via `fixtureNameFilter` (same pattern as `k3-frozen.json`) | + +### Issues Found +**CRITICAL**: None +**WARNING**: None +**SUGGESTION**: +- Worker-collector runtime MUST case asserts `reason_code` ∈ `{INSUFFICIENT_PROVENANCE, UNTRUSTED_COLLECTOR}`. Both fail closed per REQ-independent-verification-003; pinning a single code would sharpen the contract. Origin: none (non-blocking). +- Mismatched `canonicalInputs` accepts `GRAPH_DIVERGENCE` or `BINDING_MISMATCH`. Design prefers `GRAPH_DIVERGENCE`. Origin: none (non-blocking). + +4R advisory WARNINGs (FABRICATED_EVIDENCE tests, INVALID_ASSESSMENT, comments/renames) remain advisory. Covering runtime tests pass; they are not escalated to verify CRITICAL. + +### Traceability Matrix +| REQ | Tasks | Commits | Tests | Status | +|-----|-------|---------|-------|--------| +| REQ-independent-verification-005 | 4.1–4.3, 7.3 | (uncommitted working tree) | `obligation-coverage.test.js`, `index.test.js` | OK | +| REQ-independent-verification-006 | 2.1–2.3, 5.4, 7.3 | (uncommitted working tree) | `assessment.test.js`, `index.test.js` | OK | +| REQ-independent-verification-007 | 5.1–5.2, 7.3 | (uncommitted working tree) | `index.test.js` projector stub | OK | +| REQ-independent-verification-003 | 3.1–3.4, 5.3 | (uncommitted working tree) | `index.test.js` collector/provenance | OK | +| REQ-independent-verification-004 | 5.1–5.4 | (uncommitted working tree) | `index.test.js` PASS / MIXED_EVIDENCE_VERDICT / short-circuit | OK | +| REQ-assurance-graph-005 | 6.1–6.2 | (uncommitted working tree) | `assurance-graph/index.test.js` rejectForbidden | OK | +| REQ-assurance-graph-006 | 6.4–6.5, 7.1–7.2 | (uncommitted working tree) | `assurance-graph/index.test.js`, e2e replay | OK | +| REQ-assurance-graph-001 | 6.4, 7.1 | (uncommitted working tree) | reconcile + e2e C1→C2 | OK | +| REQ-assurance-graph-002 | 6.1–6.3 | (uncommitted working tree) | graph_id preimage + permutation | OK | +| REQ-kernel-contract-schemas-027 | 1.1–1.5 | (uncommitted working tree) | `k6b-schema-fixtures.test.js` | OK | +| REQ-kernel-contract-schemas-001 | 1.1, 1.4 | (uncommitted working tree) | manifest registration + inherited kN fixture suites | OK | + +No Conventional Commits with `Ospec-Change` / `Ospec-Task` trailers exist yet for this change; implementation is still in the working tree. `models.yaml` is excluded from the candidate. + +### Assumption Reconciliation +| id | statement | reversibility | outcome | +|----|-----------|----------------|---------| +| sdd-propose-001 | The persistable binding is an additive assessment/binding family; the exact schema $id is left to sdd-design. | high | resolved (sdd-design: `ospec://schemas/kernel/assessment/v1`; no escalation) | + +### Verdict +PASS +All 30 tasks complete; 35/35 MUST scenarios have runtime-test or accepted static-proof; `npm test` exited 0 (2754 pass, 0 fail); focal suite 75/75 pass. Successor 4R lineage approved with 0 BLOCKER / 0 CRITICAL. diff --git a/openspec/config.yaml b/openspec/config.yaml index ba65639b..5f39e6f1 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -11,7 +11,7 @@ context: | project: name: ospec-workflow - version: 2.50.0 + version: 2.51.0 status: active artifact_store: diff --git a/openspec/specs/assurance-graph/spec.md b/openspec/specs/assurance-graph/spec.md index eae0c40a..eb39af97 100644 --- a/openspec/specs/assurance-graph/spec.md +++ b/openspec/specs/assurance-graph/spec.md @@ -12,11 +12,14 @@ invalidation on successor preserves independent evidence. ### 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, and -verification decisions. Consumers MUST treat it as read-only. The graph MUST -NOT grant lifecycle, approval, or delivery authority. Divergence from -OpenSpec/Git/Candidate MUST fail closed. K6b MUST NOT populate `reviewed-by` -edges or treat Evaluation Attestation or Delivery Authorization as authority. +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 @@ -29,18 +32,37 @@ edges or treat Evaluation Attestation or Delivery Authorization as authority. - GIVEN an Assurance Graph that cannot be recomputed from current OpenSpec/Git/Candidate - WHEN reconciliation runs -- THEN validation MUST fail closed +- 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: Reproducible Digest And K6b Edges {#REQ-assurance-graph-002} -Identical canonical inputs MUST produce the same graph digest and the same -`AssuranceEdge` set. Each K6b edge MUST have `from`, `to`, and `relation` in -`verified-by | satisfies | derived-from | invalidates`. Edge serialization -order MUST NOT change the digest. 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. +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 @@ -55,6 +77,13 @@ or K10 authorization as authoritative subjects. - 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: Selective Invalidation Closure {#REQ-assurance-graph-003} When a Candidate successor appears or a source subject changes, the system @@ -98,3 +127,42 @@ authorization. - GIVEN an equivalence manifest payload - WHEN validated against CandidateEvaluationAttestation or DeliveryAuthorization schemas - THEN validation MUST fail closed + +### 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. + +#### 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 +- 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 diff --git a/openspec/specs/independent-verification/spec.md b/openspec/specs/independent-verification/spec.md index 382d35e1..2399d93f 100644 --- a/openspec/specs/independent-verification/spec.md +++ b/openspec/specs/independent-verification/spec.md @@ -79,15 +79,23 @@ fallback. 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. 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 -`runtime-observed`, `host-attested`, or `tool-produced` evidence. +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 raw test evidence with that provenance bound to the frozen CandidateId and graph node +- 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` @@ -107,18 +115,32 @@ A worker `model-reported` claim MUST NOT satisfy an obligation that requires - 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: 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. 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. +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 admissible provenance on a frozen CandidateId +- 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` @@ -129,3 +151,78 @@ replace the Strict TDD fallback. - 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 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 +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. + +#### 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 + +### 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 + +- 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 +- 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 + +### 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` diff --git a/openspec/specs/kernel-contract-schemas/spec.md b/openspec/specs/kernel-contract-schemas/spec.md index 4b4ee448..6129af8a 100644 --- a/openspec/specs/kernel-contract-schemas/spec.md +++ b/openspec/specs/kernel-contract-schemas/spec.md @@ -10,8 +10,8 @@ consumer tags — without executing the lifecycle reducer. ### 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, and assurance-graph. 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: K6a closed the inventory at containment-violation; K6b adds assurance-graph and additive evidence/v2 and verification/v2 without replacing K1 evidence/v1 or verification/v1 pins.) +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 @@ -77,69 +77,12 @@ 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 and verification/v2 MUST be pinnable without mutating K1 evidence/v1 or verification/v1 - - -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, and containment-violation. 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: K5 added execution-budget, authority-effect-budget, causal-failure, and failure-recovery-transition families; K6a adds workspace-descriptor, capsule-definition, work-result-execution-payload, and containment-violation families.) - -#### 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 +#### Scenario: Assessment/binding family is included without mutating K6b pins - 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 - ---- +- 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: Valid And Invalid Fixtures Per Schema Family {#REQ-kernel-contract-schemas-002} @@ -863,3 +806,42 @@ digests. - 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}$`), 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. + +#### Scenario: Valid assessment fixture passes + +- GIVEN a complete assessment/binding payload with role, obligation_id, node_id, evidence_id, and policy-snapshot identity +- 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: 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 +- THEN those schema and fixture bytes and K1 pins MUST remain byte-identical diff --git a/package.json b/package.json index 4bdf7b37..cfae6270 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ospec-workflow", - "version": "2.50.0", + "version": "2.51.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-evidence-alias.json b/schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json new file mode 100644 index 00000000..05f928b1 --- /dev/null +++ b/schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json @@ -0,0 +1,10 @@ +{ + "schema_version": 2, + "kind": "evidence/v2", + "evidence_id": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "candidate_id": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "provenance": "runtime-observed", + "origin": "npm test", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "node_id": "repair-core" +} diff --git a/schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json b/schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json new file mode 100644 index 00000000..7f040988 --- /dev/null +++ b/schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json @@ -0,0 +1,5 @@ +{ + "schema_version": 1, + "kind": "assessment/v1", + "assessment_id": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +} diff --git a/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json b/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json new file mode 100644 index 00000000..29caa795 --- /dev/null +++ b/schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json @@ -0,0 +1,12 @@ +{ + "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", + "verdict": "PASS" +} diff --git a/schemas/kernel/assessment/fixtures/valid/v1-complete.json b/schemas/kernel/assessment/fixtures/valid/v1-complete.json new file mode 100644 index 00000000..220455c6 --- /dev/null +++ b/schemas/kernel/assessment/fixtures/valid/v1-complete.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/valid/v1-four-roles.json b/schemas/kernel/assessment/fixtures/valid/v1-four-roles.json new file mode 100644 index 00000000..66c16f71 --- /dev/null +++ b/schemas/kernel/assessment/fixtures/valid/v1-four-roles.json @@ -0,0 +1,46 @@ +[ + { + "schema_version": 1, + "kind": "assessment/v1", + "assessment_id": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "evidence_id": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "role": "acceptance", + "obligation_id": "req-repair-001", + "node_id": "repair-core", + "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + }, + { + "schema_version": 1, + "kind": "assessment/v1", + "assessment_id": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "evidence_id": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "role": "invariants", + "obligation_id": "req-repair-001", + "node_id": "repair-core", + "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + }, + { + "schema_version": 1, + "kind": "assessment/v1", + "assessment_id": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "evidence_id": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "role": "contract", + "obligation_id": "req-repair-001", + "node_id": "repair-core", + "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + }, + { + "schema_version": 1, + "kind": "assessment/v1", + "assessment_id": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "evidence_id": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "role": "negative", + "obligation_id": "req-repair-001", + "node_id": "repair-core", + "candidate_id": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "policy_snapshot_id": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + } +] diff --git a/schemas/kernel/assessment/v1.schema.json b/schemas/kernel/assessment/v1.schema.json new file mode 100644 index 00000000..831bfdf0 --- /dev/null +++ b/schemas/kernel/assessment/v1.schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "ospec://schemas/kernel/assessment/v1", + "title": "AssessmentV1", + "description": "Persistable assessment/binding distinct from evidence observation and verification verdict. Verdict is forbidden.", + "type": "object", + "schema_version": 1, + "required": [ + "schema_version", + "kind", + "assessment_id", + "evidence_id", + "role", + "obligation_id", + "node_id", + "candidate_id", + "policy_snapshot_id" + ], + "properties": { + "schema_version": { + "type": "integer", + "const": 1 + }, + "kind": { + "type": "string", + "const": "assessment/v1" + }, + "assessment_id": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "evidence_id": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "role": { + "type": "string", + "minLength": 1 + }, + "obligation_id": { + "type": "string", + "minLength": 1 + }, + "node_id": { + "type": "string", + "minLength": 1 + }, + "candidate_id": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "policy_snapshot_id": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + } + }, + "additionalProperties": false +} diff --git a/schemas/kernel/assurance-graph/v1.schema.json b/schemas/kernel/assurance-graph/v1.schema.json index bd3f4c22..05569965 100644 --- a/schemas/kernel/assurance-graph/v1.schema.json +++ b/schemas/kernel/assurance-graph/v1.schema.json @@ -43,6 +43,9 @@ }, "equivalence_manifest": { "$ref": "#/$defs/equivalenceManifest" + }, + "canonical_inputs": { + "$ref": "#/$defs/canonicalInputs" } }, "additionalProperties": false, @@ -123,6 +126,28 @@ } }, "additionalProperties": false + }, + "canonicalInputs": { + "type": "object", + "additionalProperties": false, + "properties": { + "contract_digest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "policy_snapshot_id": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "execution_graph_digest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "openspec_input_digest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + } + } } } } diff --git a/schemas/kernel/contract-claims.json b/schemas/kernel/contract-claims.json index a80c4cbc..31d2ecc9 100644 --- a/schemas/kernel/contract-claims.json +++ b/schemas/kernel/contract-claims.json @@ -678,6 +678,21 @@ ] }, "command_shapes": [] + }, + "assessment": { + "required_fields": [ + "schema_version", + "kind", + "assessment_id", + "evidence_id", + "role", + "obligation_id", + "node_id", + "candidate_id", + "policy_snapshot_id" + ], + "enum_values": {}, + "command_shapes": [] } } } diff --git a/schemas/kernel/manifest.json b/schemas/kernel/manifest.json index 52b508cd..3936794a 100644 --- a/schemas/kernel/manifest.json +++ b/schemas/kernel/manifest.json @@ -220,6 +220,11 @@ "path": "schemas/kernel/assurance-graph/v1.schema.json", "$id": "ospec://schemas/kernel/assurance-graph/v1", "schema_version": 1 + }, + "assessment": { + "path": "schemas/kernel/assessment/v1.schema.json", + "$id": "ospec://schemas/kernel/assessment/v1", + "schema_version": 1 } } } diff --git a/scripts/k6b-verifier-assurance-graph-e2e.test.js b/scripts/k6b-verifier-assurance-graph-e2e.test.js index fa213249..5af01922 100644 --- a/scripts/k6b-verifier-assurance-graph-e2e.test.js +++ b/scripts/k6b-verifier-assurance-graph-e2e.test.js @@ -12,6 +12,8 @@ const { verifyCandidate } = require("./lib/independent-verifier/index.js"); const { projectAssuranceGraph, computeInvalidationClosure, + replayAssuranceGraph, + reconcileAssuranceGraph, } = require("./lib/assurance-graph/index.js"); const CONFIG_PATH = path.resolve(__dirname, "..", "openspec", "config.yaml"); @@ -49,6 +51,8 @@ function featureEvidence() { ]; } +const HARNESS_COLLECTOR = { id: "node-test", transport: "tool-execution-transport" }; + test("E2E K6b: K4b-frozen Candidate → verify → project twice → successor invalidation rejects stale evidence", () => { const configBefore = fs.readFileSync(CONFIG_PATH, "utf8"); const files = { "src/index.js": "function add(a, b) { return a + b; }\nmodule.exports = { add };\n" }; @@ -87,30 +91,63 @@ test("E2E K6b: K4b-frozen Candidate → verify → project twice → successor i contract, repository: { files }, declaredStrategy: "feature", + collector: HARNESS_COLLECTOR, rawEvidence: featureEvidence(), }); assert.equal(verified.ok, true, verified.error || verified.reason_code); assert.equal(verified.verification.verdict, "PASS"); assert.equal(verified.evidence.some((ev) => Object.prototype.hasOwnProperty.call(ev, "verdict")), false); - const classified = verified.evidence.map((evidence) => ({ evidence, obligation_ids: ["req-repair-001"] })); + const classified = verified.evidence.map((evidence) => ({ evidence })); const firstGraph = projectAssuranceGraph({ candidate: predecessor, executionGraph, evidence: classified, + assessments: verified.assessments, verification: verified.verification, + canonicalInputs: verified.assurance_graph.canonical_inputs, }); const secondGraph = projectAssuranceGraph({ candidate: predecessor, executionGraph, evidence: [...classified].reverse(), + assessments: [...verified.assessments].reverse(), verification: verified.verification, + canonicalInputs: verified.assurance_graph.canonical_inputs, }); assert.equal(firstGraph.ok, true); assert.equal(secondGraph.ok, true); assert.equal(firstGraph.graph.graph_id, secondGraph.graph.graph_id); assert.deepEqual(firstGraph.graph.edges, secondGraph.graph.edges); assert.equal(verified.assurance_graph.graph_id, firstGraph.graph.graph_id); + assert.ok(Array.isArray(verified.assessments) && verified.assessments.length >= 1); + assert.ok(verified.assurance_graph.canonical_inputs); + + const replayed = replayAssuranceGraph({ + candidate: predecessor, + executionGraph, + evidence: verified.evidence, + assessments: verified.assessments, + verification: verified.verification, + canonical_inputs: verified.assurance_graph.canonical_inputs, + }); + assert.equal(replayed.ok, true); + assert.equal(replayed.graph.graph_id, verified.assurance_graph.graph_id); + assert.deepEqual(replayed.graph.edges, verified.assurance_graph.edges); + + const churned = reconcileAssuranceGraph(verified.assurance_graph, { + candidate: predecessor, + executionGraph, + evidence: verified.evidence, + assessments: verified.assessments, + verification: verified.verification, + canonicalInputs: { + ...verified.assurance_graph.canonical_inputs, + contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + }); + assert.equal(churned.ok, false); + assert.equal(churned.reason_code, "GRAPH_DIVERGENCE"); const successorFiles = { "src/index.js": "function add(a, b) { return a + b + 1; }\nmodule.exports = { add };\n" }; const successorTree = computeTreeDigest(successorFiles); @@ -156,6 +193,7 @@ test("E2E K6b: K4b-frozen Candidate → verify → project twice → successor i contract, repository: { files }, declaredStrategy: "feature", + collector: HARNESS_COLLECTOR, rawEvidence: featureEvidence(), priorAssuranceGraph: graphForClosure, }); diff --git a/scripts/lib/assurance-graph/index.js b/scripts/lib/assurance-graph/index.js index 86f67cd4..400d3755 100644 --- a/scripts/lib/assurance-graph/index.js +++ b/scripts/lib/assurance-graph/index.js @@ -1,6 +1,6 @@ "use strict"; -const { projectAssuranceGraph } = require("./projector.js"); +const { projectAssuranceGraph, rejectForbidden } = require("./projector.js"); const { computeInvalidationClosure, isEvidenceTransitivelyInvalidated } = require("./invalidation.js"); function fail(reason_code, error) { @@ -8,7 +8,7 @@ function fail(reason_code, error) { } /** - * Recompute the projection from canonical inputs and fail closed on divergence. + * Recompute the projection from persistable outputs and fail closed on divergence. * * @param {object} stored * @param {object} canonicalInput @@ -30,6 +30,24 @@ function reconcileAssuranceGraph(stored, canonicalInput) { return { ok: true, graph: projected.graph }; } +/** + * Replay a projection from persistable assessments, evidence, verification, and canonical_inputs. + * Never consumes ephemeral projector obligation_ids. + * + * @param {object} persistable + * @returns {{ ok: true, graph: object } | { ok: false, reason_code: string }} + */ +function replayAssuranceGraph(persistable = {}) { + return projectAssuranceGraph({ + canonicalInputs: persistable.canonical_inputs || persistable.canonicalInputs, + candidate: persistable.candidate, + executionGraph: persistable.executionGraph, + evidence: persistable.evidence, + assessments: persistable.assessments, + verification: persistable.verification, + }); +} + function emitEquivalenceManifest(graph) { if (!graph || typeof graph.graph_id !== "string" || typeof graph.candidate_id !== "string") { return fail("GRAPH_DIVERGENCE", "manifest requires graph_id and candidate_id"); @@ -57,6 +75,8 @@ function rejectAuthorityMisuse(_intent) { module.exports = { projectAssuranceGraph, reconcileAssuranceGraph, + replayAssuranceGraph, + rejectForbidden, computeInvalidationClosure, isEvidenceTransitivelyInvalidated, emitEquivalenceManifest, diff --git a/scripts/lib/assurance-graph/index.test.js b/scripts/lib/assurance-graph/index.test.js index 7a4c35b8..b31bc70c 100644 --- a/scripts/lib/assurance-graph/index.test.js +++ b/scripts/lib/assurance-graph/index.test.js @@ -9,6 +9,8 @@ const { computeTreeDigest } = require("../worker-workspace.js"); const { projectAssuranceGraph, reconcileAssuranceGraph, + replayAssuranceGraph, + rejectForbidden, computeInvalidationClosure, emitEquivalenceManifest, rejectAuthorityMisuse, @@ -81,6 +83,8 @@ function featureRaw() { ]; } +const HARNESS_COLLECTOR = { id: "node-test", transport: "tool-execution-transport" }; + function verifiedProjection() { const files = { "src/index.js": "module.exports = 1;\n" }; const candidate = freezeFromFiles(files); @@ -90,6 +94,7 @@ function verifiedProjection() { executionGraph, repository: { files }, declaredStrategy: "feature", + collector: HARNESS_COLLECTOR, rawEvidence: featureRaw(), }); assert.equal(verified.ok, true, verified.error || verified.reason_code); @@ -100,19 +105,20 @@ test("REQ-assurance-graph-002: same inputs yield the same digest and edges despi const { candidate, executionGraph, verified } = verifiedProjection(); const classified = verified.evidence.map((evidence, index) => ({ evidence, - obligation_ids: ["req-repair-001"], role: ["acceptance", "invariants", "contract", "negative"][index], })); const first = projectAssuranceGraph({ candidate, executionGraph, evidence: classified, + assessments: verified.assessments, verification: verified.verification, }); const second = projectAssuranceGraph({ candidate, executionGraph, evidence: [...classified].reverse(), + assessments: [...verified.assessments].reverse(), verification: verified.verification, additionalEdges: [...(first.graph.edges || [])].reverse(), }); @@ -148,14 +154,16 @@ test("REQ-assurance-graph-001: matching canonical inputs project; divergence fai const projected = projectAssuranceGraph({ candidate, executionGraph, - evidence: verified.evidence.map((evidence) => ({ evidence, obligation_ids: ["req-repair-001"] })), + evidence: verified.evidence, + assessments: verified.assessments, verification: verified.verification, }); assert.equal(projected.ok, true); const reconciled = reconcileAssuranceGraph(projected.graph, { candidate, executionGraph, - evidence: verified.evidence.map((evidence) => ({ evidence, obligation_ids: ["req-repair-001"] })), + evidence: verified.evidence, + assessments: verified.assessments, verification: verified.verification, }); assert.equal(reconciled.ok, true); @@ -164,7 +172,8 @@ test("REQ-assurance-graph-001: matching canonical inputs project; divergence fai const diverged = reconcileAssuranceGraph(mutated, { candidate, executionGraph, - evidence: verified.evidence.map((evidence) => ({ evidence, obligation_ids: ["req-repair-001"] })), + evidence: verified.evidence, + assessments: verified.assessments, verification: verified.verification, }); assert.equal(diverged.ok, false); @@ -178,7 +187,8 @@ test("REQ-assurance-graph-001: matching canonical inputs project; divergence fai { candidate, executionGraph, - evidence: verified.evidence.map((evidence) => ({ evidence, obligation_ids: ["req-repair-001"] })), + evidence: verified.evidence, + assessments: verified.assessments, verification: verified.verification, } ); @@ -193,7 +203,8 @@ test("REQ-harness-authority-canon-010: APIs return new objects without write-thr const projected = projectAssuranceGraph({ candidate, executionGraph, - evidence: verified.evidence.map((evidence) => ({ evidence, obligation_ids: ["req-repair-001"] })), + evidence: verified.evidence, + assessments: verified.assessments, verification: verified.verification, additionalNodes: nodes, }); @@ -291,3 +302,129 @@ test("REQ-harness-authority-canon-010: graph used as approval or delivery author assert.equal(result.ok, false); assert.equal(result.reason_code, "GRAPH_AUTHORITY_MISUSE"); }); + +test("REQ-assurance-graph-002: graph_id changes when canonical inputs change; permutation does not", () => { + const { candidate, executionGraph, verified } = verifiedProjection(); + const baseInput = { + candidate, + executionGraph, + evidence: verified.evidence, + assessments: verified.assessments, + verification: verified.verification, + }; + const base = projectAssuranceGraph(baseInput); + assert.equal(base.ok, true); + assert.ok(base.graph.canonical_inputs); + + const flippedContract = projectAssuranceGraph({ + ...baseInput, + canonicalInputs: { + ...base.graph.canonical_inputs, + contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + }); + assert.equal(flippedContract.ok, true); + assert.notEqual(base.graph.graph_id, flippedContract.graph.graph_id); + + const flippedPolicy = projectAssuranceGraph({ + ...baseInput, + canonicalInputs: { + ...base.graph.canonical_inputs, + policy_snapshot_id: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + }, + }); + assert.notEqual(base.graph.graph_id, flippedPolicy.graph.graph_id); + + const flippedExec = projectAssuranceGraph({ + ...baseInput, + canonicalInputs: { + ...base.graph.canonical_inputs, + execution_graph_digest: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + }, + }); + assert.notEqual(base.graph.graph_id, flippedExec.graph.graph_id); + + const flippedOpenspec = projectAssuranceGraph({ + ...baseInput, + canonicalInputs: { + ...base.graph.canonical_inputs, + openspec_input_digest: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + }, + }); + assert.notEqual(base.graph.graph_id, flippedOpenspec.graph.graph_id); + + const permutedNodes = projectAssuranceGraph({ + ...baseInput, + additionalNodes: [...base.graph.nodes].reverse(), + }); + assert.equal(permutedNodes.ok, true); + assert.equal(base.graph.graph_id, permutedNodes.graph.graph_id); +}); + +test("REQ-assurance-graph-005: rejectForbidden matches kind/namespace, not id substring", () => { + const allowed = rejectForbidden( + [{ id: "REQ-add-authorization-header", kind: "requirement" }], + [] + ); + assert.equal(allowed.ok, true); + + const structured = rejectForbidden([{ id: "authz-1", kind: "authorization" }], []); + assert.equal(structured.ok, false); + assert.equal(structured.reason_code, "FORBIDDEN_RELATION"); + + const namespaced = rejectForbidden( + [{ id: "harmless-id", kind: "requirement", namespace: "attestation" }], + [] + ); + assert.equal(namespaced.ok, false); + assert.equal(namespaced.reason_code, "FORBIDDEN_RELATION"); +}); + +test("REQ-assurance-graph-001: missing candidate is GRAPH_PROJECTION_FAILED", () => { + const result = projectAssuranceGraph({}); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "GRAPH_PROJECTION_FAILED"); +}); + +test("REQ-assurance-graph-006: replay from persistable outputs is byte-identical; contract churn diverges", () => { + 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 replayed = replayAssuranceGraph(persistable); + assert.equal(replayed.ok, true); + assert.equal(replayed.graph.graph_id, verified.assurance_graph.graph_id); + assert.deepEqual(replayed.graph.edges, verified.assurance_graph.edges); + + const churned = projectAssuranceGraph({ + candidate, + executionGraph, + evidence: verified.evidence, + assessments: verified.assessments, + verification: verified.verification, + canonicalInputs: { + ...verified.assurance_graph.canonical_inputs, + contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + }); + assert.notEqual(churned.graph.graph_id, verified.assurance_graph.graph_id); + const diverged = reconcileAssuranceGraph(verified.assurance_graph, { + candidate, + executionGraph, + evidence: verified.evidence, + assessments: verified.assessments, + verification: verified.verification, + canonicalInputs: { + ...verified.assurance_graph.canonical_inputs, + contract_digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + }); + assert.equal(diverged.ok, false); + assert.equal(diverged.reason_code, "GRAPH_DIVERGENCE"); +}); + diff --git a/scripts/lib/assurance-graph/projector.js b/scripts/lib/assurance-graph/projector.js index dfc5f209..40ed2406 100644 --- a/scripts/lib/assurance-graph/projector.js +++ b/scripts/lib/assurance-graph/projector.js @@ -12,13 +12,19 @@ const ALLOWED_NODE_KINDS = Object.freeze([ "test-evidence", "verification-decision", ]); -const FORBIDDEN_NODE_KIND_MARKERS = Object.freeze([ +const FORBIDDEN_KINDS = Object.freeze([ "finding", "attestation", "authorization", "evaluation-attestation", - "reviewed-by", ]); +const FORBIDDEN_NAMESPACES = Object.freeze([ + "finding", + "attestation", + "authorization", + "evaluation-attestation", +]); +const SHA256 = /^sha256:[a-f0-9]{64}$/; function fail(reason_code, error) { return { ok: false, reason_code, error: error || reason_code }; @@ -56,17 +62,26 @@ function canonicalize(nodes, edges) { return { nodes: canonicalNodes, edges: canonicalEdges }; } +/** + * Accept or reject subjects by structured kind/namespace. Never scan id substrings. + * + * @param {object[]} nodes + * @param {object[]} edges + * @returns {{ ok: true } | { ok: false, reason_code: string, error?: string }} + */ function rejectForbidden(nodes, edges) { + // One reason_code for kind, namespace, and relation; distinguish via error text. + // FORBIDDEN_KINDS is defense-in-depth: checked before the allowlist so it is reachable. for (const node of nodes || []) { + if (FORBIDDEN_KINDS.includes(node.kind)) { + return fail("FORBIDDEN_RELATION", `forbidden kind ${node.kind}`); + } if (!ALLOWED_NODE_KINDS.includes(node.kind)) { return fail("FORBIDDEN_RELATION", `forbidden node kind ${node.kind}`); } - // Substring match on id+kind haystack: markers apply with includes() over - // id or kind, not exact kind equality. ALLOWED_NODE_KINDS already covers - // the exact kind allow-list above. - const idAndKindHaystack = `${node.id} ${node.kind}`.toLowerCase(); - if (FORBIDDEN_NODE_KIND_MARKERS.some((marker) => idAndKindHaystack.includes(marker))) { - return fail("FORBIDDEN_RELATION", `forbidden subject ${node.id}`); + const namespace = typeof node.namespace === "string" ? node.namespace.toLowerCase() : ""; + if (namespace && FORBIDDEN_NAMESPACES.includes(namespace)) { + return fail("FORBIDDEN_RELATION", `forbidden namespace ${node.namespace}`); } } for (const edge of edges || []) { @@ -87,6 +102,39 @@ function pushEdge(edges, from, relation, to) { edges.push({ from, relation, to }); } +function resolveCanonicalInputDigests(input) { + const provided = input.canonicalInputs && typeof input.canonicalInputs === "object" ? input.canonicalInputs : {}; + const graph = input.executionGraph || {}; + const contract = provided.contract && typeof provided.contract === "object" ? provided.contract : {}; + + const contractDigest = provided.contract_digest || contract.contract_digest || graph.contract_digest || null; + const policySnapshotId = provided.policy_snapshot_id || graph.policy_snapshot_id || null; + const executionGraphDigest = provided.execution_graph_digest || graph.graph_id || null; + const openspecInputDigest = + provided.openspec_input_digest || + sha256Fingerprint("openspec-input/v1", { + contract_digest: contractDigest, + source_snapshot_id: graph.source_snapshot_id || (provided.sourceSnapshot && provided.sourceSnapshot.source_snapshot_id) || null, + }); + + return { + 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]; + } + } + return persistable; +} + /** * Derive a canonical Assurance Graph projection. Returns a new object. * @@ -96,7 +144,7 @@ function pushEdge(edges, from, relation, to) { function projectAssuranceGraph(input = {}) { const candidate = input.candidate; if (!candidate || typeof candidate.candidate_id !== "string") { - return fail("GRAPH_DIVERGENCE", "frozen candidate is required to project"); + return fail("GRAPH_PROJECTION_FAILED", "frozen candidate is required to project"); } const nodes = []; @@ -128,11 +176,16 @@ function projectAssuranceGraph(input = {}) { if (!record || !record.evidence_id) continue; pushNode(nodes, record.evidence_id, "test-evidence"); pushEdge(edges, record.evidence_id, "derived-from", candidateId); - const obligationIds = item.obligation_ids || record.obligation_ids || []; - for (const obligationId of obligationIds) { - pushNode(nodes, obligationId, "requirement"); - pushEdge(edges, record.evidence_id, "satisfies", obligationId); - } + } + + // Persistable assessments become evidence→obligation `satisfies` edges. + // Assessment is not a node; distinct roles of the same pair collapse via canonicalize. + const assessments = Array.isArray(input.assessments) ? input.assessments : []; + for (const assessment of assessments) { + if (!assessment || !assessment.evidence_id || !assessment.obligation_id) continue; + pushNode(nodes, assessment.evidence_id, "test-evidence"); + pushNode(nodes, assessment.obligation_id, "requirement"); + pushEdge(edges, assessment.evidence_id, "satisfies", assessment.obligation_id); } const verification = input.verification; @@ -155,27 +208,36 @@ function projectAssuranceGraph(input = {}) { if (!forbidden.ok) return forbidden; const canonical = canonicalize(nodes, edges); + const canonicalInputs = resolveCanonicalInputDigests(input); const graphId = sha256Fingerprint("assurance-graph/v1", { 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, nodes: canonical.nodes, edges: canonical.edges, }); - return { - ok: true, - graph: { - schema_version: 1, - kind: "assurance-graph/v1", - graph_id: graphId, - candidate_id: candidateId, - nodes: canonical.nodes.map(cloneNode), - edges: canonical.edges.map(cloneEdge), - }, + const resultGraph = { + schema_version: 1, + kind: "assurance-graph/v1", + graph_id: graphId, + candidate_id: candidateId, + nodes: canonical.nodes.map(cloneNode), + edges: canonical.edges.map(cloneEdge), }; + const persistedInputs = persistableCanonicalInputs(canonicalInputs); + if (Object.keys(persistedInputs).length > 0) { + resultGraph.canonical_inputs = persistedInputs; + } + + return { ok: true, graph: resultGraph }; } module.exports = { ALLOWED_RELATIONS, canonicalize, + rejectForbidden, projectAssuranceGraph, }; diff --git a/scripts/lib/contract-checkers/k1-schema-compat.js b/scripts/lib/contract-checkers/k1-schema-compat.js index 4bab9f62..45830648 100644 --- a/scripts/lib/contract-checkers/k1-schema-compat.js +++ b/scripts/lib/contract-checkers/k1-schema-compat.js @@ -67,6 +67,10 @@ const FAMILY_PUBLICATION = Object.freeze({ verification: Object.freeze({ fixtureNameFilter: (name) => !name.startsWith("v2-"), }), + assessment: Object.freeze({ + // Bundle of four role payloads for identity distinctness; walked by k6b-schema-fixtures. + fixtureNameFilter: (name) => name !== "v1-four-roles.json", + }), }); function toPosix(relativePath) { diff --git a/scripts/lib/independent-verifier/assessment.js b/scripts/lib/independent-verifier/assessment.js new file mode 100644 index 00000000..8910bf8f --- /dev/null +++ b/scripts/lib/independent-verifier/assessment.js @@ -0,0 +1,77 @@ +"use strict"; + +const path = require("node:path"); +const { sha256Fingerprint } = require("../canonical-json.js"); +const { validateInstance, loadSchemaById } = require("../kernel-schema-validator.js"); + +const ASSESSMENT_V1_ID = "ospec://schemas/kernel/assessment/v1"; +const DEFAULT_SCHEMA_ROOT = path.resolve(__dirname, "../../.."); +const SHA256 = /^sha256:[a-f0-9]{64}$/; + +let cachedAssessmentSchema = null; + +function fail(reason_code, error) { + return { ok: false, reason_code, error: error || reason_code }; +} + +function getAssessmentSchema() { + if (!cachedAssessmentSchema) { + cachedAssessmentSchema = loadSchemaById(ASSESSMENT_V1_ID, { rootDir: DEFAULT_SCHEMA_ROOT }); + } + return cachedAssessmentSchema; +} + +function computeAssessmentId(fields) { + return sha256Fingerprint("assessment/v1", { + schema_version: fields.schema_version, + kind: fields.kind, + evidence_id: fields.evidence_id, + role: fields.role, + obligation_id: fields.obligation_id, + node_id: fields.node_id, + candidate_id: fields.candidate_id, + policy_snapshot_id: fields.policy_snapshot_id, + }); +} + +/** + * Emit an assessment/v1 binding. Verdict is forbidden. + * + * @param {object} input + * @returns {{ ok: true, assessment: object } | { ok: false, reason_code: string, error?: string }} + */ +function emitAssessment(input) { + if (!input || typeof input !== "object") { + return fail("INVALID_ASSESSMENT", "assessment input must be an object"); + } + if (Object.prototype.hasOwnProperty.call(input, "verdict")) { + return fail("MIXED_ASSESSMENT_VERDICT", "assessment must not carry verdict"); + } + + const record = { + schema_version: 1, + kind: "assessment/v1", + evidence_id: input.evidence_id, + role: input.role, + obligation_id: input.obligation_id, + node_id: input.node_id, + candidate_id: input.candidate_id, + policy_snapshot_id: input.policy_snapshot_id, + }; + 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 }; +} + +module.exports = { + computeAssessmentId, + emitAssessment, +}; diff --git a/scripts/lib/independent-verifier/assessment.test.js b/scripts/lib/independent-verifier/assessment.test.js new file mode 100644 index 00000000..601fd962 --- /dev/null +++ b/scripts/lib/independent-verifier/assessment.test.js @@ -0,0 +1,45 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const test = require("node:test"); + +const { computeAssessmentId, emitAssessment } = require("./assessment.js"); + +const BASE = { + schema_version: 1, + kind: "assessment/v1", + evidence_id: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + obligation_id: "req-repair-001", + node_id: "repair-core", + candidate_id: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + policy_snapshot_id: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", +}; + +test("REQ-independent-verification-006: computeAssessmentId includes role and obligation_id", () => { + const a = computeAssessmentId({ ...BASE, role: "acceptance" }); + const b = computeAssessmentId({ ...BASE, role: "invariants" }); + assert.match(a, /^sha256:[a-f0-9]{64}$/); + assert.notEqual(a, b); +}); + +test("REQ-independent-verification-006: emitAssessment validates and rejects verdict", () => { + const emitted = emitAssessment({ ...BASE, role: "acceptance" }); + assert.equal(emitted.ok, true, emitted.error); + assert.equal(emitted.assessment.kind, "assessment/v1"); + assert.equal(emitted.assessment.role, "acceptance"); + assert.equal(Object.prototype.hasOwnProperty.call(emitted.assessment, "verdict"), false); + + const withVerdict = emitAssessment({ ...BASE, role: "acceptance", verdict: "PASS" }); + assert.equal(withVerdict.ok, false); + assert.equal(withVerdict.reason_code, "MIXED_ASSESSMENT_VERDICT"); +}); + +test("REQ-independent-verification-006: four roles share evidence_id and produce four assessment_id values", () => { + const roles = ["acceptance", "invariants", "contract", "negative"]; + const emitted = roles.map((role) => emitAssessment({ ...BASE, role })); + assert.equal(emitted.every((item) => item.ok), true); + const evidenceIds = new Set(emitted.map((item) => item.assessment.evidence_id)); + const assessmentIds = new Set(emitted.map((item) => item.assessment.assessment_id)); + assert.equal(evidenceIds.size, 1); + assert.equal(assessmentIds.size, 4); +}); diff --git a/scripts/lib/independent-verifier/collector-provenance.js b/scripts/lib/independent-verifier/collector-provenance.js new file mode 100644 index 00000000..9ce1b4b4 --- /dev/null +++ b/scripts/lib/independent-verifier/collector-provenance.js @@ -0,0 +1,86 @@ +"use strict"; + +const STRONG_CLASSES = Object.freeze(["runtime-observed", "host-attested", "tool-produced"]); +const WEAK_CLASSES = Object.freeze(["model-reported", "human-decision", "external-unverified"]); + +const ALLOWLIST = Object.freeze([ + { + ids: Object.freeze(["node-test", "npm-test", "node:test"]), + transport: "tool-execution-transport", + provenanceClass: "runtime-observed", + }, + { + ids: Object.freeze(["tool-execution"]), + transport: "tool-execution-transport", + provenanceClass: "tool-produced", + }, + { + ids: Object.freeze(["host-adapter"]), + transport: "execution-transport", + provenanceClass: "host-attested", + }, +]); + +function fail(reason_code, error) { + return { ok: false, reason_code, error: error || reason_code }; +} + +function deriveProvenanceClass(collector) { + if (!collector || typeof collector !== "object") { + return { provenance: null, trusted: false, worker: false }; + } + const id = collector.id; + const transport = collector.transport; + if (id === "worker" || transport === "worker-transport") { + return { provenance: "model-reported", trusted: false, worker: true }; + } + for (const row of ALLOWLIST) { + if (row.ids.includes(id) && row.transport === transport) { + return { provenance: row.provenanceClass, trusted: true, worker: false }; + } + } + return { provenance: null, trusted: false, worker: false }; +} + +/** + * Stored class comes from the harness collector channel (`provenanceClass` on the + * allowlist row), not from silently upgrading a payload claim. The allowlisted + * collector is the authority of the stored class even when the envelope claims a + * weaker provenance. `collectorResolution` is that channel result, not the raw + * collector object. + */ +function resolveEvidenceProvenance(raw, harnessCollector) { + // Channel identity is only the harness argument. A collector field on the + // worker envelope is rejected, not ignored or merged with the channel. + if (raw && Object.prototype.hasOwnProperty.call(raw, "collector")) return fail("UNTRUSTED_COLLECTOR"); + const claimed = raw && raw.provenance; + const collectorResolution = deriveProvenanceClass(harnessCollector); + const claimsStrong = STRONG_CLASSES.includes(claimed); + const claimedClass = typeof claimed === "string" && claimed.length > 0 ? claimed : null; + + if (claimsStrong && !collectorResolution.trusted) { + return fail("UNTRUSTED_COLLECTOR", "strong provenance requires an allowlisted collector/transport"); + } + if (collectorResolution.trusted) { + if (claimedClass && claimedClass !== collectorResolution.provenance) { + return fail("UNTRUSTED_COLLECTOR", "claimed provenance disagrees with collector-derived class"); + } + return { ok: true, provenance: collectorResolution.provenance }; + } + if (collectorResolution.worker) { + return { ok: true, provenance: "model-reported" }; + } + if (WEAK_CLASSES.includes(claimed)) { + return { ok: true, provenance: claimed }; + } + if (claimsStrong) { + return fail("UNTRUSTED_COLLECTOR", "strong provenance requires an allowlisted collector/transport"); + } + return fail("UNTRUSTED_COLLECTOR", "collector/transport metadata is absent or untrusted"); +} + +module.exports = { + STRONG_CLASSES, + deriveProvenanceClass, + resolveEvidenceProvenance, +}; diff --git a/scripts/lib/independent-verifier/evidence.js b/scripts/lib/independent-verifier/evidence.js index 125e1bd4..9f1aff12 100644 --- a/scripts/lib/independent-verifier/evidence.js +++ b/scripts/lib/independent-verifier/evidence.js @@ -4,6 +4,7 @@ const crypto = require("node:crypto"); const path = require("node:path"); const { sha256Fingerprint } = require("../canonical-json.js"); const { validateInstance, loadSchemaById } = require("../kernel-schema-validator.js"); +const { resolveEvidenceProvenance } = require("./collector-provenance.js"); const EVIDENCE_V2_ID = "ospec://schemas/kernel/evidence/v2"; const DEFAULT_SCHEMA_ROOT = path.resolve(__dirname, "../../.."); @@ -55,7 +56,7 @@ function computeEvidenceId(fields, rawBytes) { * @param {object} [executionGraph] * @returns {{ ok: true, evidence: object, role?: string, obligation_ids: string[] } | { ok: false, reason_code: string }} */ -function normalizeEvidence(raw, candidate, executionGraph) { +function normalizeEvidence(raw, candidate, executionGraph, harnessCollector) { if (!raw || typeof raw !== "object") { return fail("FABRICATED_EVIDENCE", "raw evidence must be an object"); } @@ -85,11 +86,14 @@ function normalizeEvidence(raw, candidate, executionGraph) { } } + const resolvedProvenance = resolveEvidenceProvenance(raw, harnessCollector); + if (!resolvedProvenance.ok) return resolvedProvenance; + const record = { schema_version: 2, kind: "evidence/v2", candidate_id: candidateId, - provenance: raw.provenance, + provenance: resolvedProvenance.provenance, origin: raw.origin, digest, node_id: nodeId, @@ -129,14 +133,14 @@ function isRuntimeClass(provenance) { * Model-reported claims cannot satisfy runtime/tool obligations. * * @param {object} evidence - * @param {{ requireRuntime?: boolean }} [obligation] + * @param {{ requireRuntime?: boolean }} [options] * @returns {{ ok: true } | { ok: false, reason_code: string }} */ -function evaluateProvenanceSufficiency(evidence, obligation = {}) { +function evaluateProvenanceSufficiency(evidence, options = {}) { if (!evidence || typeof evidence !== "object") { return fail("INSUFFICIENT_PROVENANCE"); } - const requireRuntime = obligation.requireRuntime !== false; + const requireRuntime = options.requireRuntime !== false; if (requireRuntime && !isRuntimeClass(evidence.provenance)) { return fail("INSUFFICIENT_PROVENANCE", "model-reported cannot satisfy runtime/tool obligations"); } diff --git a/scripts/lib/independent-verifier/index.js b/scripts/lib/independent-verifier/index.js index b7f607b1..e63db276 100644 --- a/scripts/lib/independent-verifier/index.js +++ b/scripts/lib/independent-verifier/index.js @@ -2,46 +2,121 @@ const { validateBindings } = require("./bindings.js"); const { selectStrategy, evaluateStrategy } = require("./strategy-policy.js"); -const { normalizeEvidence } = require("./evidence.js"); +const { normalizeEvidence, computeEvidenceId } = require("./evidence.js"); +const { resolveEvidenceProvenance } = require("./collector-provenance.js"); const { emitVerification } = require("./verdict.js"); -const { - projectAssuranceGraph, - emitEquivalenceManifest, - isEvidenceTransitivelyInvalidated, -} = require("../assurance-graph/index.js"); +const { walkMustObligations } = require("./obligation-coverage.js"); +const assuranceGraph = require("../assurance-graph/index.js"); function fail(reason_code, error) { return { ok: false, reason_code, error: error || reason_code }; } +function channelCollector(input, index) { + if (Array.isArray(input.collectors)) return input.collectors[index]; + return input.collector; +} + +function mapProjectionFailure(projected) { + if (projected.reason_code === "GRAPH_DIVERGENCE") { + return fail("GRAPH_DIVERGENCE", projected.error); + } + return fail("GRAPH_PROJECTION_FAILED", projected.error || "GRAPH_PROJECTION_FAILED"); +} + +function bindCanonicalInputs(input, bound) { + const provided = input.canonicalInputs; + if (!provided || typeof provided !== "object") return { ok: true }; + const graph = bound.executionGraph; + const contractDigest = graph.contract_digest || (input.contract && input.contract.contract_digest); + const mismatches = [ + ["contract_digest", provided.contract_digest, contractDigest], + ["policy_snapshot_id", provided.policy_snapshot_id, graph.policy_snapshot_id], + ["execution_graph_digest", provided.execution_graph_digest, graph.graph_id], + ]; + for (const [name, providedValue, boundValue] of mismatches) { + if (typeof providedValue === "string" && boundValue && providedValue !== boundValue) { + return fail("GRAPH_DIVERGENCE", `canonicalInputs.${name} does not match the bound graph/contract`); + } + } + return { ok: true }; +} + +function rejectStaleEvidence(input, bound, evidence, rawBytes) { + const predecessorId = bound.candidate && bound.candidate.predecessor_id; + const graph = input.priorAssuranceGraph; + if (predecessorId && !graph) { + return fail("STALE_EVIDENCE", "predecessor-bound candidate requires prior Assurance Graph"); + } + if (!graph) return { ok: true }; + if (assuranceGraph.isEvidenceTransitivelyInvalidated(graph, evidence.evidence_id)) { + return fail("STALE_EVIDENCE", "evidence is reachable through a transitive invalidates edge"); + } + if (predecessorId) { + // Remint the evidence digest under the predecessor CandidateId. A copy of + // invalidated predecessor bytes with a successor-bound evidence_id still + // hits the prior graph or the invalidates closure and must fail STALE_EVIDENCE. + const predecessorBoundId = computeEvidenceId({ ...evidence, candidate_id: predecessorId }, rawBytes); + const priorIds = new Set((graph.nodes || []).map((node) => node && node.id)); + if ( + priorIds.has(predecessorBoundId) || + assuranceGraph.isEvidenceTransitivelyInvalidated(graph, predecessorBoundId) + ) { + return fail("STALE_EVIDENCE", "reminted predecessor digest remains stale under invalidates"); + } + } + return { ok: true }; +} + /** * Independently verify a frozen Candidate v2. * Worker narrative is not authority. Evidence stays distinct from verdict. * * @param {object} input - * @returns {{ ok: boolean, strategy?: string, evidence?: object[], verification?: object, reason_code?: string }} + * @returns {{ ok: boolean, strategy?: string, evidence?: object[], assessments?: object[], verification?: object, reason_code?: string }} */ function verifyCandidate(input) { const bound = validateBindings(input); if (!bound.ok) return bound; + const canonicalBinding = bindCanonicalInputs(input, bound); + if (!canonicalBinding.ok) return canonicalBinding; + const strategy = selectStrategy(input.declaredStrategy); const rawList = Array.isArray(input.rawEvidence) ? input.rawEvidence : []; const classified = []; - for (const raw of rawList) { - const normalized = normalizeEvidence(raw, bound.candidate, bound.executionGraph); + for (let index = 0; index < rawList.length; index += 1) { + const raw = rawList[index]; + const channel = channelCollector(input, index); + const provenanceGate = resolveEvidenceProvenance(raw, channel); + if (!provenanceGate.ok) return provenanceGate; + const normalized = normalizeEvidence(raw, bound.candidate, bound.executionGraph, channel); if (!normalized.ok) return normalized; - if (input.priorAssuranceGraph && isEvidenceTransitivelyInvalidated(input.priorAssuranceGraph, normalized.evidence.evidence_id)) { - return fail("STALE_EVIDENCE", "evidence is reachable through a transitive invalidates edge"); - } + const stale = rejectStaleEvidence( + input, + bound, + normalized.evidence, + raw.bytes !== undefined ? raw.bytes : raw.rawBytes + ); + if (!stale.ok) return stale; classified.push(normalized); } const evaluated = evaluateStrategy(strategy, classified); if (!evaluated.ok) return evaluated; + const coverage = walkMustObligations({ + classified, + executionGraph: bound.executionGraph, + candidate: bound.candidate, + policySnapshotId: bound.executionGraph.policy_snapshot_id, + }); + if (!coverage.ok) return coverage; + const evidenceRecords = classified.map((item) => item.evidence); + // human-decision and external-unverified extras keep a passing verification at + // PASS WITH WARNINGS. model-reported is omitted here: it cannot satisfy a runtime MUST. const hasNonRuntimeExtra = classified.some( (item) => item.evidence.provenance === "external-unverified" || item.evidence.provenance === "human-decision" ); @@ -52,7 +127,7 @@ function verifyCandidate(input) { verdict, }); - const projected = projectAssuranceGraph({ + const projected = assuranceGraph.projectAssuranceGraph({ canonicalInputs: input.canonicalInputs || { contract: input.contract, sourceSnapshot: input.sourceSnapshot, @@ -60,20 +135,22 @@ function verifyCandidate(input) { candidate: bound.candidate, executionGraph: bound.executionGraph, evidence: classified, + assessments: coverage.assessments, verification, }); + if (!projected.ok) { + return mapProjectionFailure(projected); + } - const result = { + return { ok: true, strategy, evidence: evidenceRecords, + assessments: coverage.assessments, verification, + assurance_graph: projected.graph, + equivalence_manifest: assuranceGraph.emitEquivalenceManifest(projected.graph), }; - if (projected.ok) { - result.assurance_graph = projected.graph; - result.equivalence_manifest = emitEquivalenceManifest(projected.graph); - } - return result; } module.exports = { diff --git a/scripts/lib/independent-verifier/index.test.js b/scripts/lib/independent-verifier/index.test.js index 4e2f56ad..2b816e39 100644 --- a/scripts/lib/independent-verifier/index.test.js +++ b/scripts/lib/independent-verifier/index.test.js @@ -11,6 +11,8 @@ const { computeTreeDigest } = require("../worker-workspace.js"); const { verifyCandidate, selectStrategy } = require("./index.js"); const { computeEvidenceId, digestRawBytes } = require("./evidence.js"); const { computeVerificationId } = require("./verdict.js"); +const { computeAssessmentId } = require("./assessment.js"); +const assuranceGraph = require("../assurance-graph/index.js"); const ROOT = path.resolve(__dirname, "../../.."); const CONFIG_PATH = path.join(ROOT, "openspec", "config.yaml"); @@ -73,19 +75,39 @@ function buildHarness(overrides = {}) { policySnapshot, contract, repository: { files }, + collector: Object.prototype.hasOwnProperty.call(overrides, "collector") + ? overrides.collector + : trustedCollector("runtime-observed"), }; } +function trustedCollector(provenance) { + if (provenance === "tool-produced") { + return { id: "tool-execution", transport: "tool-execution-transport" }; + } + if (provenance === "host-attested") { + return { id: "host-adapter", transport: "execution-transport" }; + } + if (provenance === "runtime-observed") { + return { id: "node-test", transport: "tool-execution-transport" }; + } + return undefined; +} + function raw(role, bytes, extra = {}) { - return { + const provenance = extra.provenance || "runtime-observed"; + const record = { role, bytes, - provenance: extra.provenance || "runtime-observed", + provenance, origin: extra.origin || `role:${role}`, node_id: extra.node_id || "repair-core", obligation_ids: extra.obligation_ids || ["req-repair-001"], - ...extra.fields, }; + if (Object.prototype.hasOwnProperty.call(extra, "collector") && extra.collector) { + record.collector = extra.collector; + } + return { ...record, ...extra.fields }; } function featureEvidence() { @@ -328,6 +350,7 @@ test("REQ-independent-verification-003: runtime-observed satisfies; model-report const modelReported = verifyCandidate({ ...harness, + collector: undefined, declaredStrategy: "feature", rawEvidence: featureEvidence().map((item) => ({ ...item, provenance: "model-reported" })), }); @@ -389,6 +412,29 @@ test("REQ-independent-verification-003: stale, foreign, or fabricated evidence i assert.equal(staleDependent.verification, undefined); }); +test("F-ad61b7e3cff9629a: predecessor remint without prior graph and digest reuse under invalidates are STALE", () => { + const files = { "src/index.js": "module.exports = 1;\n" }; + const predecessor = buildHarness({ files }); + const first = verifyCandidate({ ...predecessor, declaredStrategy: "feature", rawEvidence: featureEvidence() }); + assert.equal(first.ok, true, first.reason_code); + const successor = buildHarness({ + files, + diff_hash: "sha256:2222222222222222222222222222222222222222222222222222222222222222", + predecessorCandidate: predecessor.candidate, + }); + assert.equal(verifyCandidate({ ...successor, declaredStrategy: "feature", rawEvidence: featureEvidence() }).reason_code, "STALE_EVIDENCE"); + const reminted = verifyCandidate({ + ...successor, + declaredStrategy: "feature", + rawEvidence: featureEvidence(), + priorAssuranceGraph: { + ...first.assurance_graph, + edges: [...(first.assurance_graph.edges || []), { from: successor.candidate.candidate_id, relation: "invalidates", to: first.evidence[0].evidence_id }], + }, + }); + assert.equal(reminted.reason_code, "STALE_EVIDENCE"); +}); + test("REQ-independent-verification-004: sufficient evidence yields a verification verdict without embedding it in evidence", () => { const harness = buildHarness(); const result = verifyCandidate({ @@ -406,9 +452,11 @@ test("REQ-independent-verification-004: sufficient evidence yields a verificatio test("REQ-independent-verification-004: extra human-decision evidence yields PASS WITH WARNINGS", () => { const harness = buildHarness(); + const nodeTest = harness.collector; const result = verifyCandidate({ ...harness, declaredStrategy: "feature", + collectors: [...featureEvidence().map(() => nodeTest), undefined], rawEvidence: [ ...featureEvidence(), raw("annotation", "human reviewed", { provenance: "human-decision" }), @@ -434,7 +482,7 @@ test("REQ-independent-verification-002: feature anyOf requires contract or integ }); test("REQ-independent-verification-002: Strict TDD rejects host-attested red and green", () => { - const harness = buildHarness(); + const harness = buildHarness({ collector: { id: "host-adapter", transport: "execution-transport" } }); const result = verifyCandidate({ ...harness, rawEvidence: [ @@ -502,3 +550,279 @@ test("REQ-independent-verification-003/004: evidence_id and verification_id are computeVerificationId(first.verification.candidate_id, first.verification.verdict, first.verification.evidence_ids) ); }); + +test("REQ-independent-verification-003: payload runtime-observed without collector fails UNTRUSTED_COLLECTOR", () => { + const harness = buildHarness({ collector: undefined }); + const result = verifyCandidate({ + ...harness, + declaredStrategy: "feature", + rawEvidence: featureEvidence(), + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "UNTRUSTED_COLLECTOR"); + assert.equal(Object.prototype.hasOwnProperty.call(result, "verification"), false); +}); + +test("REQ-independent-verification-003: allowlisted node-test collector derives runtime-observed", () => { + const harness = buildHarness(); + const result = verifyCandidate({ + ...harness, + declaredStrategy: "feature", + rawEvidence: featureEvidence(), + }); + assert.equal(result.ok, true, result.error || result.reason_code); + assert.equal(result.evidence.every((ev) => ev.provenance === "runtime-observed"), true); + assert.equal(result.evidence.every((ev) => !Object.prototype.hasOwnProperty.call(ev, "collector")), true); +}); + +test("REQ-independent-verification-003: worker collector is model-reported and insufficient for runtime MUST", () => { + const harness = buildHarness({ collector: { id: "worker", transport: "worker-transport" } }); + const result = verifyCandidate({ + ...harness, + declaredStrategy: "feature", + rawEvidence: featureEvidence().map((item) => ({ ...item, provenance: "model-reported" })), + }); + assert.equal(result.ok, false); + assert.ok(["INSUFFICIENT_PROVENANCE", "UNTRUSTED_COLLECTOR"].includes(result.reason_code)); +}); + +test("REQ-independent-verification-003: payload strong vs collector weak fails closed", () => { + const harness = buildHarness({ collector: { id: "worker", transport: "worker-transport" } }); + const result = verifyCandidate({ + ...harness, + declaredStrategy: "feature", + rawEvidence: featureEvidence(), + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "UNTRUSTED_COLLECTOR"); +}); + +test("F-d5739d79237afeb8/F-2fc6db350f5b8afc: weak+allowlisted fails closed; mapping and npm-test/node:test", () => { + const harness = buildHarness(); + const run = (rawEvidence, collector) => verifyCandidate({ + ...harness, + collector, + declaredStrategy: "feature", + rawEvidence, + }); + const nodeTest = { id: "node-test", transport: "tool-execution-transport" }; + for (const provenance of ["model-reported", "human-decision", "external-unverified"]) { + const result = run(featureEvidence().map((item) => ({ ...item, provenance })), nodeTest); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "UNTRUSTED_COLLECTOR"); + } + assert.equal(run(featureEvidence(), { id: "tool-execution", transport: "tool-execution-transport" }).reason_code, "UNTRUSTED_COLLECTOR"); + assert.equal(run(featureEvidence(), { id: "node-test", transport: "execution-transport" }).reason_code, "UNTRUSTED_COLLECTOR"); + for (const id of ["npm-test", "node:test"]) { + const result = run(featureEvidence(), { id, transport: "tool-execution-transport" }); + assert.equal(result.ok, true, result.reason_code); + assert.equal(result.evidence.every((ev) => ev.provenance === "runtime-observed"), true); + } +}); + +test("F-d5739d79237afeb8: envelope collector fails closed; harness collector derives class", () => { + const h = { ...buildHarness(), declaredStrategy: "feature", collector: { id: "node-test", transport: "tool-execution-transport" } }; + assert.equal(verifyCandidate({ ...h, rawEvidence: featureEvidence().map((i) => ({ ...i, collector: h.collector })) }).reason_code, "UNTRUSTED_COLLECTOR"); + const ok = verifyCandidate({ ...h, rawEvidence: featureEvidence() }); + assert.equal(ok.ok && ok.evidence.every((e) => e.provenance === "runtime-observed"), true, ok.reason_code); +}); + +test("REQ-independent-verification-005: MUST without bound evidence fails UNFULFILLED_MUST after strategy", () => { + const harness = buildHarness(); + const result = verifyCandidate({ + ...harness, + declaredStrategy: "feature", + rawEvidence: featureEvidence().map((item) => ({ ...item, obligation_ids: [] })), + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "UNFULFILLED_MUST"); + assert.match(result.error || "", /req-repair-001/); + assert.equal(Object.prototype.hasOwnProperty.call(result, "verification"), false); +}); + +test("REQ-independent-verification-005: alien obligation_id fails UNKNOWN_OBLIGATION_ID", () => { + const harness = buildHarness(); + const result = verifyCandidate({ + ...harness, + declaredStrategy: "feature", + rawEvidence: featureEvidence().map((item, index) => + index === 0 ? { ...item, obligation_ids: ["not-in-manifest"] } : item + ), + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "UNKNOWN_OBLIGATION_ID"); +}); + +test("REQ-independent-verification-005: evidence on a non-implementing node fails WRONG_IMPLEMENTING_NODE", () => { + const extraNode = { + node_id: "other-node", + kind: "repair-action/v1", + operation: "apply_repair_patch", + objective: "Other", + dependencies: [], + ownership: { owner: "agent:repair", mode: "exclusive" }, + allowed_paths: ["src/other.js"], + invariants: ["inv-fail-closed"], + required_evidence: ["ev:test-pass"], + budget_ref: "budget:default", + }; + const files = { "src/index.js": "module.exports = 1;\n", "src/other.js": "module.exports = 2;\n" }; + const harness = buildHarness({ files }); + const { compileExecutionGraph, createPolicySnapshot } = require("../execution-graph/index.js"); + const policySnapshot = createPolicySnapshot({ effectiveRules: ["rule-fail-closed"] }); + const executionGraph = compileExecutionGraph({ + contract: harness.contract, + policySnapshot, + nodes: [...SAMPLE_NODES, extraNode], + obligations: SAMPLE_OBLIGATIONS, + }); + const result = verifyCandidate({ + ...harness, + executionGraph, + policySnapshot, + declaredStrategy: "feature", + rawEvidence: featureEvidence().map((item) => ({ ...item, node_id: "other-node" })), + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "WRONG_IMPLEMENTING_NODE"); +}); + +test("REQ-independent-verification-005: approved deferral skips MUST coverage", () => { + const deferred = [ + { + id: "req-repair-001", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: ["ev:test-pass"], + deferred: { reason: "Hardening deferred with maintainer approval", approved_by: "maintainer" }, + }, + ]; + const harness = buildHarness(); + const { compileExecutionGraph, createPolicySnapshot } = require("../execution-graph/index.js"); + const policySnapshot = createPolicySnapshot({ effectiveRules: ["rule-fail-closed"] }); + const contract = { ...harness.contract, obligations: deferred }; + const executionGraph = compileExecutionGraph({ + contract, + policySnapshot, + nodes: SAMPLE_NODES, + obligations: deferred, + }); + const result = verifyCandidate({ + ...harness, + contract, + executionGraph, + policySnapshot, + declaredStrategy: "feature", + rawEvidence: featureEvidence().map((item) => ({ ...item, obligation_ids: [] })), + }); + assert.equal(result.ok, true, result.error || result.reason_code); +}); + +test("REQ-independent-verification-007: projector failure is GRAPH_PROJECTION_FAILED without PASS or graph", () => { + const harness = buildHarness(); + const original = assuranceGraph.projectAssuranceGraph; + const run = (reason_code) => { + assuranceGraph.projectAssuranceGraph = () => ({ ok: false, reason_code, error: "stub" }); + return verifyCandidate({ ...harness, declaredStrategy: "feature", rawEvidence: featureEvidence() }); + }; + try { + const failed = run("GRAPH_PROJECTION_FAILED"); + assert.equal(failed.ok, false); + assert.equal(failed.reason_code, "GRAPH_PROJECTION_FAILED"); + assert.equal(Object.prototype.hasOwnProperty.call(failed, "assurance_graph"), false); + assert.equal(Object.prototype.hasOwnProperty.call(failed, "verification"), false); + const diverged = run("GRAPH_DIVERGENCE"); + assert.equal(diverged.ok, false); + assert.equal(diverged.reason_code, "GRAPH_DIVERGENCE"); + assert.equal(Object.prototype.hasOwnProperty.call(diverged, "verification"), false); + } finally { + assuranceGraph.projectAssuranceGraph = original; + } +}); + +test("F-6b1f8c8265c82b3e: mismatched canonicalInputs fail closed", () => { + const harness = buildHarness(); + const mismatch = "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + for (const canonicalInputs of [{ contract_digest: mismatch }, { policy_snapshot_id: mismatch }, { execution_graph_digest: mismatch }]) { + const result = verifyCandidate({ ...harness, declaredStrategy: "feature", rawEvidence: featureEvidence(), canonicalInputs }); + assert.equal(result.ok, false); + assert.ok(["GRAPH_DIVERGENCE", "BINDING_MISMATCH"].includes(result.reason_code)); + assert.equal(Object.prototype.hasOwnProperty.call(result, "verification"), false); + } +}); + +test("REQ-independent-verification-004: strategy failure short-circuits without MUST upgrade", () => { + const harness = buildHarness(); + const result = verifyCandidate({ + ...harness, + declaredStrategy: "feature", + rawEvidence: [ + raw("acceptance", "acceptance"), + raw("invariants", "invariants"), + raw("contract", "contract"), + ], + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "MISSING_NEGATIVE"); + assert.notEqual(result.reason_code, "UNFULFILLED_MUST"); +}); + +test("REQ-independent-verification-006: four roles over one observation yield four assessments and one evidence_id", () => { + const harness = buildHarness(); + const shared = { origin: "shared-observation", obligation_ids: ["req-repair-001"] }; + const bytes = "shared-bytes"; + const result = verifyCandidate({ + ...harness, + declaredStrategy: "feature", + rawEvidence: [ + raw("acceptance", bytes, shared), + raw("invariants", bytes, shared), + raw("contract", bytes, shared), + 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, + }) + ); +}); + +test("FABRICATED_EVIDENCE: non-object raw and missing origin fail closed", () => { + const harness = buildHarness(); + const nonObject = verifyCandidate({ + ...harness, + declaredStrategy: "feature", + rawEvidence: [null, ...featureEvidence().slice(1)], + }); + assert.equal(nonObject.ok, false); + assert.equal(nonObject.reason_code, "FABRICATED_EVIDENCE"); + assert.equal(Object.prototype.hasOwnProperty.call(nonObject, "verification"), false); + + const missingOrigin = verifyCandidate({ + ...harness, + declaredStrategy: "feature", + rawEvidence: featureEvidence().map((item, index) => (index === 0 ? { ...item, origin: "" } : item)), + }); + assert.equal(missingOrigin.ok, false); + assert.equal(missingOrigin.reason_code, "FABRICATED_EVIDENCE"); + assert.equal(Object.prototype.hasOwnProperty.call(missingOrigin, "verification"), false); +}); diff --git a/scripts/lib/independent-verifier/obligation-coverage.js b/scripts/lib/independent-verifier/obligation-coverage.js new file mode 100644 index 00000000..d57a275c --- /dev/null +++ b/scripts/lib/independent-verifier/obligation-coverage.js @@ -0,0 +1,119 @@ +"use strict"; + +const { emitAssessment } = require("./assessment.js"); +const { evaluateProvenanceSufficiency } = require("./evidence.js"); + +function fail(reason_code, error) { + return { ok: false, reason_code, error: error || reason_code }; +} + +function isApprovedDeferred(obligation) { + return Boolean( + obligation && + obligation.deferred && + typeof obligation.deferred === "object" && + typeof obligation.deferred.reason === "string" && + obligation.deferred.reason.trim() !== "" && + typeof obligation.deferred.approved_by === "string" && + obligation.deferred.approved_by.trim() !== "" + ); +} + +function isMust(obligation) { + return String((obligation && obligation.criticality) || "must").toLowerCase() === "must"; +} + +/** + * Walk non-deferred MUST obligations after strategy evaluation. + * Join key is persistable obligation_id, not K4a evidence tokens. + * + * @param {{ classified: object[], executionGraph: object, candidate: object, policySnapshotId: string }} input + * @returns {{ ok: true, assessments: object[] } | { ok: false, reason_code: string, error?: string }} + */ +function walkMustObligations(input) { + const classified = Array.isArray(input && input.classified) ? input.classified : []; + const graph = input && input.executionGraph; + const candidate = input && input.candidate; + const policySnapshotId = input && input.policySnapshotId; + if (!graph || !Array.isArray(graph.obligations)) { + return fail("BINDING_MISMATCH", "executionGraph.obligations is required"); + } + const obligations = graph.obligations; + const byId = new Map(); + for (const obligation of obligations) { + if (obligation && typeof obligation.id === "string") { + byId.set(obligation.id, obligation); + } + } + + const assessments = []; + + for (const item of classified) { + const obligationIds = Array.isArray(item.obligation_ids) ? item.obligation_ids : []; + const nodeId = item.evidence && item.evidence.node_id; + for (const obligationId of obligationIds) { + const obligation = byId.get(obligationId); + if (!obligation) { + return fail("UNKNOWN_OBLIGATION_ID", `obligation_id ${obligationId} is not in the Obligation Manifest`); + } + const implementedBy = Array.isArray(obligation.implemented_by) ? obligation.implemented_by : []; + if (!implementedBy.includes(nodeId)) { + return fail( + "WRONG_IMPLEMENTING_NODE", + `node ${nodeId} does not implement obligation ${obligationId}` + ); + } + } + } + + for (const obligation of obligations) { + if (!isMust(obligation) || isApprovedDeferred(obligation)) continue; + + // Contract is non-empty presence of required_evidence, not token matching + // against its contents. Empty or missing list fails closed as UNFULFILLED_MUST. + const requiredEvidence = Array.isArray(obligation.required_evidence) ? obligation.required_evidence : []; + if (requiredEvidence.length === 0) { + return fail("UNFULFILLED_MUST", `MUST obligation ${obligation.id} has empty required_evidence`); + } + + const implementedBy = Array.isArray(obligation.implemented_by) ? obligation.implemented_by : []; + const matches = classified.filter((item) => { + const ids = Array.isArray(item.obligation_ids) ? item.obligation_ids : []; + return ids.includes(obligation.id) && implementedBy.includes(item.evidence && item.evidence.node_id); + }); + + const admissible = []; + for (const item of matches) { + const sufficiency = evaluateProvenanceSufficiency(item.evidence, { requireRuntime: true }); + if (sufficiency.ok) admissible.push(item); + } + if (admissible.length === 0) { + const boundButInadmissible = matches.length > 0; + if (boundButInadmissible) { + // Linked evidence failed the runtime provenance gate. + return fail("INSUFFICIENT_PROVENANCE", `MUST obligation ${obligation.id} lacks admissible provenance`); + } + return fail("UNFULFILLED_MUST", `MUST obligation ${obligation.id} has no admissible evidence`); + } + + for (const item of admissible) { + const emitted = emitAssessment({ + evidence_id: item.evidence.evidence_id, + role: item.role, + obligation_id: obligation.id, + node_id: item.evidence.node_id, + candidate_id: candidate && candidate.candidate_id, + policy_snapshot_id: policySnapshotId, + }); + if (!emitted.ok) return emitted; + assessments.push(emitted.assessment); + } + } + + return { ok: true, assessments }; +} + +module.exports = { + isApprovedDeferred, + walkMustObligations, +}; diff --git a/scripts/lib/independent-verifier/obligation-coverage.test.js b/scripts/lib/independent-verifier/obligation-coverage.test.js new file mode 100644 index 00000000..df1be529 --- /dev/null +++ b/scripts/lib/independent-verifier/obligation-coverage.test.js @@ -0,0 +1,272 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const test = require("node:test"); + +const { walkMustObligations } = require("./obligation-coverage.js"); + +const CANDIDATE = { candidate_id: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" }; +const POLICY = "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; +const EVIDENCE = { + evidence_id: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + provenance: "runtime-observed", + node_id: "repair-core", +}; + +function graph(obligations) { + return { obligations }; +} + +test("REQ-independent-verification-005: MUST without evidence fails UNFULFILLED_MUST", () => { + const result = walkMustObligations({ + classified: [], + executionGraph: graph([ + { + id: "req-repair-001", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: ["ev:test-pass"], + }, + ]), + candidate: CANDIDATE, + policySnapshotId: POLICY, + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "UNFULFILLED_MUST"); + assert.match(result.error, /req-repair-001/); +}); + +test("REQ-independent-verification-005: unknown obligation_id fails closed", () => { + const result = walkMustObligations({ + classified: [ + { role: "acceptance", evidence: EVIDENCE, obligation_ids: ["alien"] }, + ], + executionGraph: graph([ + { + id: "req-repair-001", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: ["ev:test-pass"], + }, + ]), + candidate: CANDIDATE, + policySnapshotId: POLICY, + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "UNKNOWN_OBLIGATION_ID"); +}); + +test("REQ-independent-verification-005: wrong implementing node fails closed", () => { + const result = walkMustObligations({ + classified: [ + { + role: "acceptance", + evidence: { ...EVIDENCE, node_id: "other-node" }, + obligation_ids: ["req-repair-001"], + }, + ], + executionGraph: graph([ + { + id: "req-repair-001", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: ["ev:test-pass"], + }, + ]), + candidate: CANDIDATE, + policySnapshotId: POLICY, + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "WRONG_IMPLEMENTING_NODE"); +}); + +test("REQ-independent-verification-005: approved deferral skips MUST", () => { + const result = walkMustObligations({ + classified: [], + executionGraph: graph([ + { + id: "req-repair-001", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: ["ev:test-pass"], + deferred: { reason: "later", approved_by: "maintainer" }, + }, + ]), + candidate: CANDIDATE, + policySnapshotId: POLICY, + }); + assert.equal(result.ok, true); + assert.deepEqual(result.assessments, []); +}); + +test("REQ-independent-verification-005: empty required_evidence on non-deferred MUST fails", () => { + const result = walkMustObligations({ + classified: [ + { role: "acceptance", evidence: EVIDENCE, obligation_ids: ["req-repair-001"] }, + ], + executionGraph: graph([ + { + id: "req-repair-001", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: [], + }, + ]), + candidate: CANDIDATE, + policySnapshotId: POLICY, + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "UNFULFILLED_MUST"); +}); + +test("REQ-independent-verification-005: strategy-shaped bindings still emit persistable assessments", () => { + const result = walkMustObligations({ + classified: [ + { role: "acceptance", evidence: EVIDENCE, obligation_ids: ["req-repair-001"] }, + ], + executionGraph: graph([ + { + id: "req-repair-001", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: ["ev:test-pass"], + }, + ]), + candidate: CANDIDATE, + policySnapshotId: POLICY, + }); + assert.equal(result.ok, true, result.error); + assert.equal(result.assessments.length, 1); + assert.equal(result.assessments[0].obligation_id, "req-repair-001"); + assert.equal(result.assessments[0].role, "acceptance"); +}); + +test("REQ-independent-verification-005: weak provenance on MUST is INSUFFICIENT_PROVENANCE", () => { + const runtimeUnbound = { + ...EVIDENCE, + evidence_id: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }; + const weakBound = { ...EVIDENCE, provenance: "model-reported" }; + const result = walkMustObligations({ + classified: [ + { role: "characterization-before", evidence: runtimeUnbound, obligation_ids: ["req-char-001"] }, + { role: "acceptance", evidence: weakBound, obligation_ids: ["req-repair-001"] }, + ], + executionGraph: graph([ + { + id: "req-repair-001", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: ["ev:test-pass"], + }, + { + id: "req-char-001", + criticality: "should", + implemented_by: ["repair-core"], + required_evidence: ["ev:char"], + }, + ]), + candidate: CANDIDATE, + policySnapshotId: POLICY, + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "INSUFFICIENT_PROVENANCE"); + assert.match(result.error, /req-repair-001/); + assert.equal(result.assessments, undefined); +}); + +test("REQ-independent-verification-005: second unfulfilled MUST is identified", () => { + const result = walkMustObligations({ + classified: [ + { role: "acceptance", evidence: EVIDENCE, obligation_ids: ["req-repair-001"] }, + ], + executionGraph: graph([ + { + id: "req-repair-001", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: ["ev:test-pass"], + }, + { + id: "req-repair-002", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: ["ev:second"], + }, + ]), + candidate: CANDIDATE, + policySnapshotId: POLICY, + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "UNFULFILLED_MUST"); + assert.match(result.error, /req-repair-002/); + assert.equal(result.assessments, undefined); +}); + +test("REQ-independent-verification-005: incomplete deferral still requires MUST coverage", () => { + const base = { + id: "req-repair-001", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: ["ev:test-pass"], + }; + const incomplete = [ + { reason: "later" }, + { approved_by: "maintainer" }, + { reason: "later", approved_by: " " }, + { reason: " ", approved_by: "maintainer" }, + ]; + for (const deferred of incomplete) { + const result = walkMustObligations({ + classified: [], + executionGraph: graph([{ ...base, deferred }]), + candidate: CANDIDATE, + policySnapshotId: POLICY, + }); + assert.equal(result.ok, false, JSON.stringify(deferred)); + assert.equal(result.reason_code, "UNFULFILLED_MUST"); + assert.match(result.error, /req-repair-001/); + assert.match(result.error, /evidence/); + assert.doesNotMatch(result.error, /assessment/i); + } +}); + +test("REQ-independent-verification-005: missing executionGraph fails closed", () => { + const missing = walkMustObligations({ + classified: [], + candidate: CANDIDATE, + policySnapshotId: POLICY, + }); + assert.equal(missing.ok, false); + assert.equal(missing.reason_code, "BINDING_MISMATCH"); + + const nonArray = walkMustObligations({ + classified: [], + executionGraph: { obligations: { id: "req-repair-001" } }, + candidate: CANDIDATE, + policySnapshotId: POLICY, + }); + assert.equal(nonArray.ok, false); + assert.equal(nonArray.reason_code, "BINDING_MISMATCH"); +}); + +test("REQ-independent-verification-005: emitAssessment failure is INVALID_ASSESSMENT", () => { + const result = walkMustObligations({ + classified: [ + { role: "", evidence: EVIDENCE, obligation_ids: ["req-repair-001"] }, + ], + executionGraph: graph([ + { + id: "req-repair-001", + criticality: "must", + implemented_by: ["repair-core"], + required_evidence: ["ev:test-pass"], + }, + ]), + candidate: CANDIDATE, + policySnapshotId: POLICY, + }); + assert.equal(result.ok, false); + assert.equal(result.reason_code, "INVALID_ASSESSMENT"); + assert.equal(result.assessments, undefined); +}); diff --git a/scripts/lib/k1-scope-guard.test.js b/scripts/lib/k1-scope-guard.test.js index 4b19e285..22e421a4 100644 --- a/scripts/lib/k1-scope-guard.test.js +++ b/scripts/lib/k1-scope-guard.test.js @@ -204,6 +204,7 @@ const SUCCESSOR_K2_PREFIXES = [ "schemas/kernel/verification/fixtures/valid/v2-", "schemas/kernel/verification/fixtures/invalid/v2-", "schemas/kernel/assurance-graph/", + "schemas/kernel/assessment/", ]; const PROTECTED_BASELINE_PATHS = [ diff --git a/scripts/lib/k6b-schema-fixtures.test.js b/scripts/lib/k6b-schema-fixtures.test.js index 83bc8fc0..19ef9e30 100644 --- a/scripts/lib/k6b-schema-fixtures.test.js +++ b/scripts/lib/k6b-schema-fixtures.test.js @@ -41,6 +41,13 @@ test("K6b schema registration: manifest indexes evidence/v2, verification/v2, an assert.equal(verificationV2.$id, "ospec://schemas/kernel/verification/v2"); const graph = loadSchemaById("ospec://schemas/kernel/assurance-graph/v1", { rootDir: ROOT }); assert.equal(graph.$id, "ospec://schemas/kernel/assurance-graph/v1"); + + assert.ok(manifest.families.assessment, "manifest must register assessment"); + assert.equal(manifest.families.assessment.schema_version, 1); + assert.equal(manifest.families.assessment.$id, "ospec://schemas/kernel/assessment/v1"); + assert.equal(manifest.families.assessment.path, "schemas/kernel/assessment/v1.schema.json"); + const assessment = loadSchemaById("ospec://schemas/kernel/assessment/v1", { rootDir: ROOT }); + assert.equal(assessment.$id, "ospec://schemas/kernel/assessment/v1"); }); test("K6b contract claims: additive families list required fields without replacing v1 claims", () => { @@ -86,6 +93,19 @@ test("K6b contract claims: additive families list required fields without replac "derived-from", "invalidates", ]); + + assert.ok(claims.families.assessment, "assessment claims must exist"); + assert.deepEqual(claims.families.assessment.required_fields, [ + "schema_version", + "kind", + "assessment_id", + "evidence_id", + "role", + "obligation_id", + "node_id", + "candidate_id", + "policy_snapshot_id", + ]); }); test("K6b evidence/v2: valid fixture passes; verdict and unknown provenance fail closed", () => { @@ -216,3 +236,60 @@ test("K6b: K1 evidence/v1 and verification/v1 files and pins remain byte-identic const result = assertK1SchemasUnchanged(ROOT); assert.equal(result.ok, true, `K1 baseline must be intact: ${JSON.stringify(result)}`); }); + +test("K6b assessment/v1: valid fixture passes; verdict, missing fields, and cross-family fail closed", () => { + const assessmentSchema = loadSchemaById("ospec://schemas/kernel/assessment/v1", { rootDir: ROOT }); + const evidenceSchema = loadSchemaById("ospec://schemas/kernel/evidence/v2", { rootDir: ROOT }); + const verificationSchema = loadSchemaById("ospec://schemas/kernel/verification/v2", { rootDir: ROOT }); + + const complete = readJson("schemas/kernel/assessment/fixtures/valid/v1-complete.json"); + const completeRes = validateInstance(assessmentSchema, complete); + assert.equal(completeRes.valid, true, `valid assessment rejected: ${JSON.stringify(completeRes.errors)}`); + + const missing = readJson("schemas/kernel/assessment/fixtures/invalid/v1-missing-required.json"); + assert.equal(validateInstance(assessmentSchema, missing).valid, false); + + const withVerdict = readJson("schemas/kernel/assessment/fixtures/invalid/v1-with-verdict.json"); + const verdictRes = validateInstance(assessmentSchema, withVerdict); + assert.equal(verdictRes.valid, false, "assessment/v1 with verdict must fail"); + assert.ok(verdictRes.errors.some((e) => /verdict|additionalProperties/i.test(e.message + e.path + e.rule))); + + const alias = readJson("schemas/kernel/assessment/fixtures/invalid/v1-evidence-alias.json"); + assert.equal(validateInstance(assessmentSchema, alias).valid, false, "evidence/v2 must not validate as assessment/v1"); + assert.equal(validateInstance(evidenceSchema, complete).valid, false, "assessment/v1 must not validate as evidence/v2"); + assert.equal(validateInstance(verificationSchema, complete).valid, false, "assessment/v1 must not validate as verification/v2"); +}); + +test("K6b assessment/v1: four roles share one evidence_id and produce distinct assessment_id values", () => { + const schema = loadSchemaById("ospec://schemas/kernel/assessment/v1", { rootDir: ROOT }); + const four = readJson("schemas/kernel/assessment/fixtures/valid/v1-four-roles.json"); + assert.ok(Array.isArray(four), "v1-four-roles.json must be an array of payloads"); + assert.equal(four.length, 4); + + const evidenceIds = new Set(); + const assessmentIds = new Set(); + const roles = new Set(); + for (const payload of four) { + const result = validateInstance(schema, payload); + assert.equal(result.valid, true, `four-role payload rejected: ${JSON.stringify(result.errors)}`); + evidenceIds.add(payload.evidence_id); + assessmentIds.add(payload.assessment_id); + roles.add(payload.role); + } + assert.equal(evidenceIds.size, 1, "four roles must share one evidence_id"); + assert.equal(assessmentIds.size, 4, "four roles must produce four assessment_id values"); + assert.equal(roles.size, 4); +}); + +test("K6b: evidence/v2 and verification/v2 schema bytes remain frozen after assessment publication", () => { + const V2_PINS = { + "schemas/kernel/evidence/v2.schema.json": + "sha256:fad66198ac48f47109041e45017e77227268610cddbb929e4dfcc3e0c5ec4910", + "schemas/kernel/verification/v2.schema.json": + "sha256:441ee351d7c094558818a3af0cfcac8b823818e5562c341d3595f2305cc4396b", + }; + for (const [rel, expected] of Object.entries(V2_PINS)) { + const actual = digestFile(path.join(ROOT, ...rel.split("/"))); + assert.equal(actual, expected, `${rel} bytes must remain frozen`); + } +}); diff --git a/scripts/lib/lifecycle-kernel/k1-compat.js b/scripts/lib/lifecycle-kernel/k1-compat.js index ea83a61f..63442c03 100644 --- a/scripts/lib/lifecycle-kernel/k1-compat.js +++ b/scripts/lib/lifecycle-kernel/k1-compat.js @@ -177,6 +177,7 @@ function listK1SchemaFiles(rootDir) { "schemas/kernel/verification/fixtures/valid/v2-", "schemas/kernel/verification/fixtures/invalid/v2-", "schemas/kernel/assurance-graph/", + "schemas/kernel/assessment/", // Evolutionary catalog registries (excluded from K1 frozen baseline pin) "schemas/kernel/manifest.json", "schemas/kernel/contract-claims.json",