From 020f17544c8e6e1a7645ae53e2003d69fa89391a Mon Sep 17 00:00:00 2001 From: xiwuqi Date: Thu, 2 Apr 2026 21:00:25 -0500 Subject: [PATCH 1/4] docs(phase-26): define checklist item attestation and attestation note paths --- ...item-attestations-and-attestation-notes.md | 102 ++++++++++++++ ...dit-catalog-checklist-item-attestations.md | 129 ++++++++++++++++++ docs/roadmap.md | 2 +- packages/config/src/index.test.ts | 4 +- packages/config/src/index.ts | 7 +- 5 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 docs/architecture/adr-0026-cross-run-audit-catalog-checklist-item-attestations-and-attestation-notes.md create mode 100644 docs/guides/audit-catalog-checklist-item-attestations.md diff --git a/docs/architecture/adr-0026-cross-run-audit-catalog-checklist-item-attestations-and-attestation-notes.md b/docs/architecture/adr-0026-cross-run-audit-catalog-checklist-item-attestations-and-attestation-notes.md new file mode 100644 index 0000000..46bd235 --- /dev/null +++ b/docs/architecture/adr-0026-cross-run-audit-catalog-checklist-item-attestations-and-attestation-notes.md @@ -0,0 +1,102 @@ +# ADR-0026: Cross-Run Audit Catalog Checklist Item Attestations and Attestation Notes + +Date: 2026-04-02 + +## Status + +Accepted on branch for Phase 26 implementation. + +## Context + +Phase 25 added a thin checklist-item-evidence layer over verified resolved +blocked progressed assigned reviewed audit catalog entries. Operators could +attach stable evidence references and a single evidence note through shared +SDK, API, CLI, and web seams, but there was still no package-owned way to +record whether that evidence had been affirmatively attested without expanding +into payload persistence, copied artifact persistence, attachment-upload, an +artifact-vault product, or approval gating. + +The next repository-owned gap is still narrower than threaded collaboration, +broader checklist orchestration, broader review workflow engines, +fine-grained RBAC, multi-tenant access, dashboards, search, analytics, or +artifact-upload products. Runroot needs a stable checklist-item-attestation +layer that remains derived over the existing checklist-item-evidence, +checklist-item-verification, checklist-item-resolution, +checklist-item-blocker, checklist-item-progress, assignment-checklist, +review-assignment, review-signal, visibility, catalog, and saved-view seams. + +## Decision + +Runroot adds a shared audit-catalog checklist-item-attestation contract with +the following properties: + +- checklist item attestations remain additive operator metadata +- checklist item attestations reference existing catalog entries, + checklist-item-evidence, checklist-item-verifications, + checklist-item-resolutions, checklist-item-blockers, + checklist-item-progress, assignment checklists, review assignments, and + review signals +- checklist item attestations store only stable per-item attestation state, a + thin optional attestation note, minimal actor and scope references, and + existing catalog refs +- checklist item attestations do not snapshot audit facts, provider payloads, + copied artifacts, or workflow state +- checklist item attestations do not change replay or approval source of truth +- applying an attested preset reuses the existing catalog-apply path + +The shared attestation seam is exposed through: + +- replay query helpers in `@runroot/replay` +- persistence adapters in `@runroot/persistence` +- operator methods in `@runroot/sdk` +- thin HTTP routes in `apps/api` +- thin command routing in `@runroot/cli` +- minimal runs-page presentation in `apps/web` + +Attestation visibility stays minimal: + +- list and inspect are scoped to operators already involved in the evidence, + verification, resolution, blocker, progress, and assignment handoff path +- attestation entries can only reference checklist items that already exist in + the shared checklist-item-evidence layer +- attestation state stays as a thin per-item enum +- attestation notes stay as a single thin string +- no threaded comments, checklist workflow engine, permission framework, + organization directory, multi-tenant surface, attachment-upload product, or + artifact vault is added + +## Consequences + +### Positive + +- evidenced verified resolved blocked progressed assigned reviewed presets can + carry thin per-item attestation state and a single attestation note through + shared package-owned seams +- inline and queued runs reuse the same checklist-item-attestation contract +- operator surfaces stay thin and reuse the existing catalog apply behavior +- replay and approval semantics remain unchanged + +### Negative + +- checklist item attestations are still a derived layer that depends on + checklist-item evidence, checklist-item verifications, checklist-item + resolutions, checklist-item blockers, checklist-item progress, assignment + checklists, assignments, review signals, visibility, and catalog integrity +- attestations intentionally stay shallow and do not solve broader checklist + orchestration, threaded collaboration, payload persistence, binary artifact + persistence, RBAC, or multi-tenant requirements + +## Non-Goals + +This ADR does not introduce: + +- threaded comments +- broader review workflow engines +- broader checklist orchestration +- attestation-driven approval gating +- attachment-upload or artifact-vault products +- fine-grained RBAC +- multi-tenant access control +- dashboard, search, or analytics products +- broader collaboration beyond thin operator-facing attestation metadata +- provider payload, copied artifact, or full snapshot persistence diff --git a/docs/guides/audit-catalog-checklist-item-attestations.md b/docs/guides/audit-catalog-checklist-item-attestations.md new file mode 100644 index 0000000..333c1dc --- /dev/null +++ b/docs/guides/audit-catalog-checklist-item-attestations.md @@ -0,0 +1,129 @@ +# Audit Catalog Checklist Item Attestations + +Phase 26 adds a thin shared checklist-item-attestation layer over evidenced +verified resolved blocked progressed assigned reviewed audit catalog entries +that already carry checklist-item-evidence metadata. + +## What Checklist Item Attestations Record + +The shared contract stores: + +- a reference to an existing evidenced verified resolved blocked progressed + assigned reviewed catalog entry +- per-item attestation state for checklist items that already exist in the + shared checklist-item-evidence layer +- minimal operator and scope references used by the current operator seam +- an optional thin attestation note + +The contract does not store: + +- provider-specific payloads +- copied artifacts +- workflow-state snapshots +- replay or approval state +- threaded comments, broader review workflow engines, or broader checklist + orchestration +- fine-grained RBAC or multi-tenant access rules +- surface-specific route formats +- attachment-upload or artifact-vault product state + +Checklist item attestations remain derived operator state. They do not replace +replay, approval, saved views, catalog entries, visibility, review signals, +review assignments, assignment checklists, checklist-item progress, +checklist-item blockers, checklist-item resolutions, checklist-item +verifications, or checklist-item evidence. + +## Attest, List-Attested, Inspect-Attestation, Clear-Attestation, And Apply + +The minimum attestation path is available through the existing seams: + +- SDK: + - `attestCatalogEntry(id, ...)` + - `listAttestedCatalogEntries()` + - `getCatalogChecklistItemAttestation(id)` + - `clearCatalogChecklistItemAttestation(id)` + - `applyCatalogEntry(id)` +- API: + - `POST /audit/catalog/:catalogEntryId/attestation` + - `GET /audit/catalog/attested` + - `GET /audit/catalog/:catalogEntryId/attestation` + - `POST /audit/catalog/:catalogEntryId/attestation/clear` + - `GET /audit/catalog/:catalogEntryId/apply` +- CLI: + - `audit catalog attest` + - `audit catalog attested` + - `audit catalog inspect-attestation` + - `audit catalog clear-attestation` + - `audit catalog apply` +- Web: + - the runs page presents a thin checklist-item-attestation panel and a + minimal attestation-note form over the existing catalog, visibility, + review-signal, review-assignment, assignment-checklist, + checklist-item-progress, checklist-item-blocker, + checklist-item-resolution, checklist-item-verification, and + checklist-item-evidence surfaces + +## What Applying An Attested Preset Does + +Applying an attested preset does not replay a run or reconstruct workflow +state. + +It only: + +- resolves the visible catalog entry for the current operator identity +- resolves additive review, assignment, checklist, progress, blocker, + resolution, verification, evidence, and attestation metadata for that entry +- resolves the referenced saved view and constrained navigation metadata +- reuses the existing catalog-apply and audit-navigation seams +- returns the current navigation state for that attested evidenced preset + +Replay and approval semantics still come only from persisted runtime and +approval events. + +## Local Development + +Set a minimal operator identity for the current process: + +```bash +$env:RUNROOT_OPERATOR_ID="ops_oncall" +$env:RUNROOT_OPERATOR_SCOPE="ops" +``` + +Create and record attestations on an evidenced verified resolved blocked +progressed assigned reviewed shared preset: + +```bash +pnpm dev:queued +pnpm --filter @runroot/cli dev audit saved-views save --name "queued worker" --execution-mode queued --worker-id worker_1 +pnpm --filter @runroot/cli dev audit catalog publish saved_view_1 --name "Queued preset" +pnpm --filter @runroot/cli dev audit catalog share catalog_entry_1 +pnpm --filter @runroot/cli dev audit catalog review catalog_entry_1 --state recommended --note "Ready for attestation" +pnpm --filter @runroot/cli dev audit catalog assign catalog_entry_1 --assignee ops_backup --handoff-note "Take the overnight follow-up" +pnpm --filter @runroot/cli dev audit catalog checklist catalog_entry_1 --status pending --items-json "[\"Validate worker state\",\"Confirm saved drilldown\"]" +pnpm --filter @runroot/cli dev audit catalog progress catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"state\":\"completed\"},{\"item\":\"Confirm saved drilldown\",\"state\":\"pending\"}]" +pnpm --filter @runroot/cli dev audit catalog block catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"state\":\"cleared\"},{\"item\":\"Confirm saved drilldown\",\"state\":\"blocked\"}]" --blocker-note "Waiting for overnight handoff" +pnpm --filter @runroot/cli dev audit catalog resolve catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"state\":\"resolved\"},{\"item\":\"Confirm saved drilldown\",\"state\":\"unresolved\"}]" --resolution-note "Backup confirmed the closeout" +pnpm --filter @runroot/cli dev audit catalog verify catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"state\":\"verified\"},{\"item\":\"Confirm saved drilldown\",\"state\":\"unverified\"}]" --verification-note "Owner verified the closeout" +pnpm --filter @runroot/cli dev audit catalog record-evidence catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"references\":[\"run://queued-worker/step/7\",\"note://backup-closeout\"]},{\"item\":\"Confirm saved drilldown\",\"references\":[\"doc://saved-drilldown\"]}]" --evidence-note "Thin evidence references only" +pnpm --filter @runroot/cli dev audit catalog attest catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"state\":\"attested\"},{\"item\":\"Confirm saved drilldown\",\"state\":\"unattested\"}]" --attestation-note "Owner attested the stable evidence references" +pnpm --filter @runroot/cli dev audit catalog attested +pnpm --filter @runroot/cli dev audit catalog apply catalog_entry_1 +pnpm --filter @runroot/cli dev audit catalog clear-attestation catalog_entry_1 +``` + +Both inline-originated and queued-originated presets reuse the same +checklist-item-attestation contract through the configured persistence +adapter. + +## What Stays Deferred + +Still out of scope after Phase 26: + +- productized dashboards, discovery products, or broad analytics UX +- open-ended search products +- fine-grained RBAC, org or team management, and multi-tenant access models +- threaded comments, broader checklist orchestration, broader review + workflows, or broader multi-user curation +- full observability backend integrations +- provider payload persistence, copied artifact persistence, attachment-upload, + or artifact-vault products diff --git a/docs/roadmap.md b/docs/roadmap.md index 9e9748e..60253b9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -404,4 +404,4 @@ Status: completed engines, broader multi-user curation, artifact-vault products, or broad observability and analytics platform work -Status: scope frozen +Status: completed diff --git a/packages/config/src/index.test.ts b/packages/config/src/index.test.ts index 9fd42a1..3021974 100644 --- a/packages/config/src/index.test.ts +++ b/packages/config/src/index.test.ts @@ -11,9 +11,9 @@ import { describe("@runroot/config", () => { it("exposes phase-aware project metadata", () => { expect(projectMetadata.name).toBe("Runroot"); - expect(projectMetadata.currentPhase).toBe(25); + expect(projectMetadata.currentPhase).toBe(26); expect(projectMetadata.phaseName).toBe( - "Cross-Run Audit Catalog Checklist Item Evidence References and Evidence Notes", + "Cross-Run Audit Catalog Checklist Item Attestations and Attestation Notes", ); }); diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index bde6a71..6ea3341 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -25,7 +25,8 @@ export type DeliveryPhase = | 22 | 23 | 24 - | 25; + | 25 + | 26; export type ExecutionMode = "inline" | "queued"; @@ -83,9 +84,9 @@ export const projectMetadata = { name: "Runroot", description: "MCP-native runtime and orchestration for durable developer and ops workflows.", - currentPhase: 25, + currentPhase: 26, phaseName: - "Cross-Run Audit Catalog Checklist Item Evidence References and Evidence Notes", + "Cross-Run Audit Catalog Checklist Item Attestations and Attestation Notes", } as const; export const requiredQualityCommands = [ From 6a3cd229703b5e8d42484290ccffc4ecafb589cb Mon Sep 17 00:00:00 2001 From: xiwuqi Date: Thu, 2 Apr 2026 21:00:37 -0500 Subject: [PATCH 2/4] feat(phase-26): add catalog checklist item attestation contract --- ...g-checklist-item-attestation-store.test.ts | 143 +++ ...atalog-checklist-item-attestation-store.ts | 870 ++++++++++++++++++ packages/persistence/src/index.ts | 16 +- packages/persistence/src/migrations.ts | 37 + .../src/checklist-item-attestation.test.ts | 98 ++ .../replay/src/checklist-item-attestation.ts | 187 ++++ packages/replay/src/index.ts | 16 + packages/replay/src/query.ts | 237 +++++ 8 files changed, 1603 insertions(+), 1 deletion(-) create mode 100644 packages/persistence/src/catalog-checklist-item-attestation-store.test.ts create mode 100644 packages/persistence/src/catalog-checklist-item-attestation-store.ts create mode 100644 packages/replay/src/checklist-item-attestation.test.ts create mode 100644 packages/replay/src/checklist-item-attestation.ts diff --git a/packages/persistence/src/catalog-checklist-item-attestation-store.test.ts b/packages/persistence/src/catalog-checklist-item-attestation-store.test.ts new file mode 100644 index 0000000..c58b55e --- /dev/null +++ b/packages/persistence/src/catalog-checklist-item-attestation-store.test.ts @@ -0,0 +1,143 @@ +import { mkdtemp } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { createCrossRunAuditCatalogChecklistItemAttestation } from "@runroot/replay"; +import { newDb } from "pg-mem"; +import { describe, expect, it } from "vitest"; + +import { + createFileAuditCatalogChecklistItemAttestationStore, + createPostgresAuditCatalogChecklistItemAttestationStore, + createSqliteAuditCatalogChecklistItemAttestationStore, + resolveAuditCatalogChecklistItemAttestationFilePath, +} from "./catalog-checklist-item-attestation-store"; + +function createAttestation( + catalogEntryId: string, + timestamp = "2026-04-02T04:15:00.000Z", + attestationNote = "Operator attested that the cited evidence is sufficient", +) { + return createCrossRunAuditCatalogChecklistItemAttestation({ + attestationNote, + catalogEntryId, + items: [ + { + item: "Validate queued follow-up", + state: "attested", + }, + { + item: "Close backup handoff", + state: "unattested", + }, + ], + operatorId: "ops_oncall", + scopeId: "ops", + timestamp, + }); +} + +describe("@runroot/persistence audit catalog checklist item attestation stores", () => { + it("persists audit catalog checklist item attestation through the Postgres adapter", async () => { + const memoryDatabase = newDb({ + noAstCoverageCheck: true, + }); + const { Pool } = memoryDatabase.adapters.createPg(); + const pool = new Pool(); + const firstStore = createPostgresAuditCatalogChecklistItemAttestationStore({ + pool, + }); + const secondStore = createPostgresAuditCatalogChecklistItemAttestationStore( + { + pool, + }, + ); + + try { + await firstStore.saveCatalogChecklistItemAttestation( + createAttestation("catalog_entry_postgres"), + ); + + expect(await secondStore.listCatalogChecklistItemAttestations()).toEqual([ + createAttestation("catalog_entry_postgres"), + ]); + } finally { + await pool.end(); + } + }); + + it("persists audit catalog checklist item attestation through the SQLite adapter", async () => { + const workspaceRoot = await mkdtemp( + join(tmpdir(), "runroot-attestation-sqlite-"), + ); + const filePath = join(workspaceRoot, "runroot.sqlite"); + const firstStore = createSqliteAuditCatalogChecklistItemAttestationStore({ + filePath, + }); + const secondStore = createSqliteAuditCatalogChecklistItemAttestationStore({ + filePath, + }); + + await firstStore.saveCatalogChecklistItemAttestation( + createAttestation("catalog_entry_sqlite"), + ); + + expect(await secondStore.listCatalogChecklistItemAttestations()).toEqual([ + createAttestation("catalog_entry_sqlite"), + ]); + }); + + it("persists audit catalog checklist item attestation through the file-sidecar compatibility path", async () => { + const workspaceRoot = await mkdtemp( + join(tmpdir(), "runroot-attestation-file-"), + ); + const workspacePath = join(workspaceRoot, "workspace.json"); + const fileStore = createFileAuditCatalogChecklistItemAttestationStore({ + filePath: + resolveAuditCatalogChecklistItemAttestationFilePath(workspacePath), + }); + + await fileStore.saveCatalogChecklistItemAttestation( + createAttestation("catalog_entry_file"), + ); + + expect(await fileStore.listCatalogChecklistItemAttestations()).toEqual([ + createAttestation("catalog_entry_file"), + ]); + }); + + it("overwrites and clears audit catalog checklist item attestation through the file-sidecar compatibility path", async () => { + const workspaceRoot = await mkdtemp( + join(tmpdir(), "runroot-attestation-file-overwrite-"), + ); + const workspacePath = join(workspaceRoot, "workspace.json"); + const fileStore = createFileAuditCatalogChecklistItemAttestationStore({ + filePath: + resolveAuditCatalogChecklistItemAttestationFilePath(workspacePath), + }); + const originalEntry = createAttestation("catalog_entry_overwrite"); + const updatedEntry = createAttestation( + "catalog_entry_overwrite", + "2026-04-02T04:15:10.000Z", + "Operator re-attested the evidence after the queued handoff", + ); + + await fileStore.saveCatalogChecklistItemAttestation(originalEntry); + await fileStore.saveCatalogChecklistItemAttestation(updatedEntry); + + expect( + await fileStore.getCatalogChecklistItemAttestation( + "catalog_entry_overwrite", + ), + ).toEqual(updatedEntry); + expect(await fileStore.listCatalogChecklistItemAttestations()).toEqual([ + updatedEntry, + ]); + expect( + await fileStore.deleteCatalogChecklistItemAttestation( + "catalog_entry_overwrite", + ), + ).toEqual(updatedEntry); + expect(await fileStore.listCatalogChecklistItemAttestations()).toEqual([]); + }); +}); diff --git a/packages/persistence/src/catalog-checklist-item-attestation-store.ts b/packages/persistence/src/catalog-checklist-item-attestation-store.ts new file mode 100644 index 0000000..985db1d --- /dev/null +++ b/packages/persistence/src/catalog-checklist-item-attestation-store.ts @@ -0,0 +1,870 @@ +import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises"; +import { dirname, join, parse, resolve } from "node:path"; + +import { + type ResolvePersistenceConfigOptions, + resolvePersistenceConfig, +} from "@runroot/config"; +import type { + CrossRunAuditCatalogChecklistItemAttestation, + CrossRunAuditCatalogChecklistItemAttestationStore, +} from "@runroot/replay"; +import { compareCrossRunAuditCatalogChecklistItemAttestation } from "@runroot/replay"; +import type { Pool, PoolClient } from "pg"; +import type { + BindParams, + Database as SqliteDatabase, + SqlJsStatic, +} from "sql.js"; +import initSqlJs from "sql.js/dist/sql-asm.js"; + +import { + migratePostgresPersistence, + migrateSqlitePersistence, + type PostgresRuntimePersistenceOptions, + type SqliteRuntimePersistenceOptions, +} from "./database-store"; + +type SqlPrimitive = number | string | null; +type SqlQueryRow = Readonly>; + +interface SqlClient { + readonly dialect: "postgres" | "sqlite"; + execute(sql: string, params?: readonly SqlPrimitive[]): Promise; + queryRows( + sql: string, + params?: readonly SqlPrimitive[], + ): Promise; +} + +type PostgresPoolLike = Pick; + +export interface InMemoryAuditCatalogChecklistItemAttestationStoreOptions { + readonly attestationEntries?: readonly CrossRunAuditCatalogChecklistItemAttestation[]; +} + +export interface FileAuditCatalogChecklistItemAttestationStoreOptions { + readonly filePath: string; + readonly lockRetryDelayMs?: number; + readonly lockTimeoutMs?: number; +} + +export interface PostgresAuditCatalogChecklistItemAttestationStoreOptions + extends Pick {} + +export interface SqliteAuditCatalogChecklistItemAttestationStoreOptions + extends Pick< + SqliteRuntimePersistenceOptions, + "filePath" | "lockRetryDelayMs" | "lockTimeoutMs" + > {} + +export interface ConfiguredAuditCatalogChecklistItemAttestationStoreOptions + extends ResolvePersistenceConfigOptions { + readonly filePath?: string; + readonly lockRetryDelayMs?: number; + readonly lockTimeoutMs?: number; + readonly pool?: PostgresPoolLike; +} + +interface AuditCatalogChecklistItemAttestationSnapshot { + readonly attestationEntries: readonly CrossRunAuditCatalogChecklistItemAttestation[]; +} + +let sqliteModulePromise: Promise | undefined; + +export function createConfiguredAuditCatalogChecklistItemAttestationStore( + options: ConfiguredAuditCatalogChecklistItemAttestationStoreOptions = {}, +): CrossRunAuditCatalogChecklistItemAttestationStore { + const resolved = resolvePersistenceConfig(options); + + switch (resolved.driver) { + case "file": + return createFileAuditCatalogChecklistItemAttestationStore({ + filePath: + options.filePath ?? + resolveAuditCatalogChecklistItemAttestationFilePath( + resolved.workspacePath ?? resolved.location, + ), + ...(options.lockRetryDelayMs !== undefined + ? { lockRetryDelayMs: options.lockRetryDelayMs } + : {}), + ...(options.lockTimeoutMs !== undefined + ? { lockTimeoutMs: options.lockTimeoutMs } + : {}), + }); + case "postgres": + return createPostgresAuditCatalogChecklistItemAttestationStore({ + ...(resolved.databaseUrl ? { databaseUrl: resolved.databaseUrl } : {}), + ...(options.pool ? { pool: options.pool } : {}), + }); + case "sqlite": + return createSqliteAuditCatalogChecklistItemAttestationStore({ + filePath: resolved.sqlitePath ?? resolved.location, + ...(options.lockRetryDelayMs !== undefined + ? { lockRetryDelayMs: options.lockRetryDelayMs } + : {}), + ...(options.lockTimeoutMs !== undefined + ? { lockTimeoutMs: options.lockTimeoutMs } + : {}), + }); + } +} + +export function createInMemoryAuditCatalogChecklistItemAttestationStore( + options: InMemoryAuditCatalogChecklistItemAttestationStoreOptions = {}, +): CrossRunAuditCatalogChecklistItemAttestationStore { + const attestationEntries = [...(options.attestationEntries ?? [])].map( + (entry) => clone(entry), + ); + + return { + async deleteCatalogChecklistItemAttestation(catalogEntryId) { + const existingIndex = attestationEntries.findIndex( + (entry) => entry.catalogEntryId === catalogEntryId, + ); + + if (existingIndex < 0) { + return undefined; + } + + const [deletedEntry] = attestationEntries.splice(existingIndex, 1); + + return deletedEntry ? clone(deletedEntry) : undefined; + }, + + async getCatalogChecklistItemAttestation(catalogEntryId) { + const attestation = attestationEntries.find( + (entry) => entry.catalogEntryId === catalogEntryId, + ); + + return attestation ? clone(attestation) : undefined; + }, + + async listCatalogChecklistItemAttestations() { + return attestationEntries + .slice() + .sort(compareCrossRunAuditCatalogChecklistItemAttestation) + .map((entry) => clone(entry)); + }, + + async saveCatalogChecklistItemAttestation(entry) { + const existingIndex = attestationEntries.findIndex( + (candidate) => candidate.catalogEntryId === entry.catalogEntryId, + ); + + if (existingIndex >= 0) { + attestationEntries.splice(existingIndex, 1); + } + + attestationEntries.push(clone(entry)); + attestationEntries.sort( + compareCrossRunAuditCatalogChecklistItemAttestation, + ); + + return clone(entry); + }, + }; +} + +export function createFileAuditCatalogChecklistItemAttestationStore( + options: FileAuditCatalogChecklistItemAttestationStoreOptions, +): CrossRunAuditCatalogChecklistItemAttestationStore { + const filePath = resolve(options.filePath); + let accessQueue = Promise.resolve(); + + return { + async deleteCatalogChecklistItemAttestation(catalogEntryId) { + return enqueueAccess(async () => + withMutableSnapshot(filePath, options, async (snapshot) => { + const existingEntry = snapshot.attestationEntries.find( + (entry) => entry.catalogEntryId === catalogEntryId, + ); + + if (!existingEntry) { + return undefined; + } + + await writeAuditCatalogChecklistItemAttestationSnapshot(filePath, { + attestationEntries: snapshot.attestationEntries.filter( + (entry) => entry.catalogEntryId !== catalogEntryId, + ), + }); + + return clone(existingEntry); + }), + ); + }, + + async getCatalogChecklistItemAttestation(catalogEntryId) { + return enqueueAccess(async () => + withReadOnlySnapshot(filePath, (snapshot) => { + const attestation = snapshot.attestationEntries.find( + (entry) => entry.catalogEntryId === catalogEntryId, + ); + + return attestation ? clone(attestation) : undefined; + }), + ); + }, + + async listCatalogChecklistItemAttestations() { + return enqueueAccess(async () => + withReadOnlySnapshot(filePath, (snapshot) => + snapshot.attestationEntries + .slice() + .sort(compareCrossRunAuditCatalogChecklistItemAttestation) + .map((entry) => clone(entry)), + ), + ); + }, + + async saveCatalogChecklistItemAttestation(entry) { + return enqueueAccess(async () => + withMutableSnapshot(filePath, options, async (snapshot) => { + const nextAttestationEntries = [ + ...snapshot.attestationEntries.filter( + (candidate) => candidate.catalogEntryId !== entry.catalogEntryId, + ), + clone(entry), + ].sort(compareCrossRunAuditCatalogChecklistItemAttestation); + + await writeAuditCatalogChecklistItemAttestationSnapshot(filePath, { + attestationEntries: nextAttestationEntries, + }); + + return clone(entry); + }), + ); + }, + }; + + async function enqueueAccess( + accessOperation: () => Promise, + ): Promise { + const pendingAccess = accessQueue.then(accessOperation, accessOperation); + accessQueue = pendingAccess.then( + () => undefined, + () => undefined, + ); + + return pendingAccess; + } +} + +export function createPostgresAuditCatalogChecklistItemAttestationStore( + options: PostgresAuditCatalogChecklistItemAttestationStoreOptions = {}, +): CrossRunAuditCatalogChecklistItemAttestationStore { + const pool = options.pool ?? createDefaultPool(options.databaseUrl); + let schemaReadyPromise: Promise | undefined; + + return createDatabaseAuditCatalogChecklistItemAttestationStore({ + ensureSchema() { + schemaReadyPromise ??= migratePostgresPersistence({ + ...(options.databaseUrl ? { databaseUrl: options.databaseUrl } : {}), + ...(options.pool ? { pool: options.pool } : {}), + }).then(() => undefined); + + return schemaReadyPromise; + }, + withReadOnlyClient(task) { + return withPostgresClient(pool, task); + }, + withTransaction(task) { + return withPostgresTransaction(pool, task); + }, + }); +} + +export function createSqliteAuditCatalogChecklistItemAttestationStore( + options: SqliteAuditCatalogChecklistItemAttestationStoreOptions, +): CrossRunAuditCatalogChecklistItemAttestationStore { + const filePath = resolve(options.filePath); + let accessQueue = Promise.resolve(); + let schemaReadyPromise: Promise | undefined; + + return createDatabaseAuditCatalogChecklistItemAttestationStore({ + ensureSchema() { + schemaReadyPromise ??= migrateSqlitePersistence({ + filePath, + }).then(() => undefined); + + return schemaReadyPromise; + }, + withReadOnlyClient(task) { + return enqueueAccess(async () => + withSqliteClient( + { + filePath, + mutable: false, + }, + task, + ), + ); + }, + withTransaction(task) { + return enqueueAccess(async () => + withFileLock(filePath, options, async () => + withSqliteClient( + { + filePath, + mutable: true, + }, + task, + ), + ), + ); + }, + }); + + async function enqueueAccess( + action: () => Promise, + ): Promise { + const pendingAccess = accessQueue.then(action, action); + accessQueue = pendingAccess.then( + () => undefined, + () => undefined, + ); + + return pendingAccess; + } +} + +function createDatabaseAuditCatalogChecklistItemAttestationStore(options: { + readonly ensureSchema: () => Promise; + readonly withReadOnlyClient: ( + task: (client: SqlClient) => Promise, + ) => Promise; + readonly withTransaction: ( + task: (client: SqlClient) => Promise, + ) => Promise; +}): CrossRunAuditCatalogChecklistItemAttestationStore { + return { + async deleteCatalogChecklistItemAttestation(catalogEntryId) { + await options.ensureSchema(); + + return options.withTransaction(async (client) => { + const existingRows = await client.queryRows<{ data: string }>( + `SELECT data + FROM runroot_audit_catalog_checklist_item_attestation + WHERE catalog_entry_id = ?`, + [catalogEntryId], + ); + + if (!existingRows[0]) { + return undefined; + } + + await client.execute( + `DELETE FROM runroot_audit_catalog_checklist_item_attestation + WHERE catalog_entry_id = ?`, + [catalogEntryId], + ); + + return deserializeRow( + existingRows[0].data, + ); + }); + }, + + async getCatalogChecklistItemAttestation(catalogEntryId) { + await options.ensureSchema(); + + return options.withReadOnlyClient(async (client) => { + const rows = await client.queryRows<{ data: string }>( + `SELECT data + FROM runroot_audit_catalog_checklist_item_attestation + WHERE catalog_entry_id = ?`, + [catalogEntryId], + ); + + return rows[0] + ? deserializeRow( + rows[0].data, + ) + : undefined; + }); + }, + + async listCatalogChecklistItemAttestations() { + await options.ensureSchema(); + + return options.withReadOnlyClient(async (client) => { + const rows = await client.queryRows<{ data: string }>( + `SELECT data + FROM runroot_audit_catalog_checklist_item_attestation`, + ); + + return rows + .map((row) => + deserializeRow( + row.data, + ), + ) + .sort(compareCrossRunAuditCatalogChecklistItemAttestation); + }); + }, + + async saveCatalogChecklistItemAttestation(entry) { + await options.ensureSchema(); + + return options.withTransaction(async (client) => { + await client.execute( + `INSERT INTO runroot_audit_catalog_checklist_item_attestation ( + catalog_entry_id, + kind, + operator_id, + scope_id, + attestation_note, + attestation_items, + created_at, + updated_at, + data + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (catalog_entry_id) DO UPDATE SET + kind = excluded.kind, + operator_id = excluded.operator_id, + scope_id = excluded.scope_id, + attestation_note = excluded.attestation_note, + attestation_items = excluded.attestation_items, + created_at = excluded.created_at, + updated_at = excluded.updated_at, + data = excluded.data`, + [ + entry.catalogEntryId, + entry.kind, + entry.operatorId, + entry.scopeId, + entry.attestationNote ?? null, + JSON.stringify(entry.items), + entry.createdAt, + entry.updatedAt, + serializeRow(entry), + ], + ); + + return clone(entry); + }); + }, + }; +} + +export function resolveAuditCatalogChecklistItemAttestationFilePath( + workspacePath: string, +): string { + const resolvedPath = resolve(workspacePath); + const parsedPath = parse(resolvedPath); + + return join( + parsedPath.dir, + `${parsedPath.name}.audit-catalog-checklist-item-attestation.json`, + ); +} + +async function withReadOnlySnapshot( + filePath: string, + action: ( + snapshot: AuditCatalogChecklistItemAttestationSnapshot, + ) => TValue | Promise, +): Promise { + const snapshot = + await readAuditCatalogChecklistItemAttestationSnapshot(filePath); + + return action(snapshot); +} + +async function withMutableSnapshot( + filePath: string, + options: Pick< + FileAuditCatalogChecklistItemAttestationStoreOptions, + "lockRetryDelayMs" | "lockTimeoutMs" + >, + action: ( + snapshot: AuditCatalogChecklistItemAttestationSnapshot, + ) => Promise, +): Promise { + await ensureParentDirectory(filePath); + + return withFileLock(filePath, options, async () => + action(await readAuditCatalogChecklistItemAttestationSnapshot(filePath)), + ); +} + +async function readAuditCatalogChecklistItemAttestationSnapshot( + filePath: string, +): Promise { + try { + const rawSnapshot = await readFile(filePath, "utf8"); + const parsedSnapshot = JSON.parse( + rawSnapshot, + ) as AuditCatalogChecklistItemAttestationSnapshot; + + return { + attestationEntries: [...(parsedSnapshot.attestationEntries ?? [])].map( + (entry) => clone(entry), + ), + }; + } catch (error) { + if (isMissingFileError(error)) { + return { + attestationEntries: [], + }; + } + + throw error; + } +} + +async function writeAuditCatalogChecklistItemAttestationSnapshot( + filePath: string, + snapshot: AuditCatalogChecklistItemAttestationSnapshot, +): Promise { + const tempPath = `${filePath}.tmp`; + const backupPath = `${filePath}.bak`; + + await writeFile(tempPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8"); + + try { + await rename(tempPath, filePath); + + return; + } catch (error) { + if (!isExistingFileError(error)) { + throw error; + } + } + + await rm(backupPath, { + force: true, + }); + + try { + await rename(filePath, backupPath); + await rename(tempPath, filePath); + } catch (error) { + await rm(tempPath, { + force: true, + }); + + try { + await rename(backupPath, filePath); + } catch (restoreError) { + if (!isMissingFileError(restoreError)) { + throw restoreError; + } + } + + throw error; + } + + await rm(backupPath, { + force: true, + }); +} + +async function withPostgresClient( + pool: PostgresPoolLike, + task: (client: SqlClient) => Promise, +): Promise { + const client = await pool.connect(); + + try { + return await task(new PostgresSqlClient(client)); + } finally { + client.release(); + } +} + +async function withPostgresTransaction( + pool: PostgresPoolLike, + task: (client: SqlClient) => Promise, +): Promise { + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + const result = await task(new PostgresSqlClient(client)); + await client.query("COMMIT"); + + return result; + } catch (error) { + await client.query("ROLLBACK"); + + throw error; + } finally { + client.release(); + } +} + +async function withSqliteClient( + options: { + readonly filePath: string; + readonly mutable: boolean; + }, + task: (client: SqlClient) => Promise, +): Promise { + if (options.mutable) { + await ensureParentDirectory(options.filePath); + } + + const SQL = await loadSqliteModule(); + const database = await openSqliteDatabase(SQL, options.filePath); + + try { + const client = new SqliteSqlClient(database); + + if (!options.mutable) { + return task(client); + } + + await client.execute("BEGIN"); + + try { + const result = await task(client); + await client.execute("COMMIT"); + await writeBinaryFileAtomically(options.filePath, database.export()); + + return result; + } catch (error) { + await client.execute("ROLLBACK"); + + throw error; + } + } finally { + database.close(); + } +} + +async function openSqliteDatabase( + SQL: SqlJsStatic, + filePath: string, +): Promise { + try { + const contents = await readFile(filePath); + + return new SQL.Database(new Uint8Array(contents)); + } catch (error) { + if (isMissingFileError(error)) { + return new SQL.Database(); + } + + throw error; + } +} + +async function loadSqliteModule(): Promise { + sqliteModulePromise ??= initSqlJs(); + + return sqliteModulePromise; +} + +class PostgresSqlClient implements SqlClient { + readonly dialect = "postgres" as const; + + constructor(private readonly client: PoolClient) {} + + async execute( + sql: string, + params: readonly SqlPrimitive[] = [], + ): Promise { + const result = await this.client.query( + convertQuestionMarksToPostgres(sql), + [...params], + ); + + return result.rowCount ?? 0; + } + + async queryRows( + sql: string, + params: readonly SqlPrimitive[] = [], + ): Promise { + const result = await this.client.query( + convertQuestionMarksToPostgres(sql), + [...params], + ); + + return result.rows; + } +} + +class SqliteSqlClient implements SqlClient { + readonly dialect = "sqlite" as const; + + constructor(private readonly database: SqliteDatabase) {} + + async execute( + sql: string, + params: readonly SqlPrimitive[] = [], + ): Promise { + this.database.run(sql, toSqliteParams(params)); + + return this.database.getRowsModified(); + } + + async queryRows( + sql: string, + params: readonly SqlPrimitive[] = [], + ): Promise { + const statement = this.database.prepare(sql, toSqliteParams(params)); + const rows: TRow[] = []; + + try { + while (statement.step()) { + rows.push(statement.getAsObject() as TRow); + } + } finally { + statement.free(); + } + + return rows; + } +} + +function createDefaultPool(databaseUrl?: string): PostgresPoolLike { + if (!databaseUrl) { + throw new Error( + 'Postgres audit catalog checklist item attestation requires DATABASE_URL or an explicit "databaseUrl" option.', + ); + } + + const { Pool } = require("pg") as typeof import("pg"); + + return new Pool({ + connectionString: databaseUrl, + }); +} + +function toSqliteParams(params: readonly SqlPrimitive[]): BindParams { + return [...params]; +} + +function convertQuestionMarksToPostgres(sql: string): string { + let placeholderIndex = 0; + + return sql.replace(/\?/g, () => `$${++placeholderIndex}`); +} + +async function ensureParentDirectory(filePath: string): Promise { + await mkdir(dirname(filePath), { + recursive: true, + }); +} + +async function writeBinaryFileAtomically( + filePath: string, + contents: Uint8Array, +): Promise { + const tempPath = `${filePath}.tmp`; + const backupPath = `${filePath}.bak`; + + await writeFile(tempPath, Buffer.from(contents)); + + try { + await rename(tempPath, filePath); + + return; + } catch (error) { + if (!isExistingFileError(error)) { + throw error; + } + } + + await rm(backupPath, { + force: true, + }); + + try { + await rename(filePath, backupPath); + await rename(tempPath, filePath); + } catch (error) { + await rm(tempPath, { + force: true, + }); + + try { + await rename(backupPath, filePath); + } catch (restoreError) { + if (!isMissingFileError(restoreError)) { + throw restoreError; + } + } + + throw error; + } + + await rm(backupPath, { + force: true, + }); +} + +async function withFileLock( + filePath: string, + options: Pick< + | FileAuditCatalogChecklistItemAttestationStoreOptions + | SqliteAuditCatalogChecklistItemAttestationStoreOptions, + "lockRetryDelayMs" | "lockTimeoutMs" + >, + action: () => Promise, +): Promise { + const lockPath = `${filePath}.lock`; + const retryDelayMs = options.lockRetryDelayMs ?? 25; + const timeoutMs = options.lockTimeoutMs ?? 5_000; + const startedAt = Date.now(); + + while (true) { + try { + const lockHandle = await open(lockPath, "wx"); + + try { + return await action(); + } finally { + await lockHandle.close(); + await rm(lockPath, { + force: true, + }); + } + } catch (error) { + if (!isExistingFileError(error)) { + throw error; + } + + if (Date.now() - startedAt >= timeoutMs) { + throw new Error( + `Timed out waiting for audit catalog checklist item attestation lock at "${lockPath}".`, + ); + } + + await delay(retryDelayMs); + } + } +} + +function delay(durationMs: number): Promise { + return new Promise((resolveDelay) => { + setTimeout(resolveDelay, durationMs); + }); +} + +function serializeRow(value: unknown): string { + return JSON.stringify(value); +} + +function deserializeRow(rawValue: string): TValue { + return JSON.parse(rawValue) as TValue; +} + +function clone(value: TValue): TValue { + return structuredClone(value); +} + +function isExistingFileError(error: unknown): boolean { + return ( + error instanceof Error && + "code" in error && + (error.code === "EEXIST" || error.code === "EPERM") + ); +} + +function isMissingFileError(error: unknown): boolean { + return error instanceof Error && "code" in error && error.code === "ENOENT"; +} diff --git a/packages/persistence/src/index.ts b/packages/persistence/src/index.ts index 03ff6e6..21a327a 100644 --- a/packages/persistence/src/index.ts +++ b/packages/persistence/src/index.ts @@ -13,6 +13,19 @@ export { resolveAuditCatalogAssignmentChecklistsFilePath, type SqliteAuditCatalogAssignmentChecklistStoreOptions, } from "./catalog-assignment-checklist-store"; +export { + type ConfiguredAuditCatalogChecklistItemAttestationStoreOptions, + createConfiguredAuditCatalogChecklistItemAttestationStore, + createFileAuditCatalogChecklistItemAttestationStore, + createInMemoryAuditCatalogChecklistItemAttestationStore, + createPostgresAuditCatalogChecklistItemAttestationStore, + createSqliteAuditCatalogChecklistItemAttestationStore, + type FileAuditCatalogChecklistItemAttestationStoreOptions, + type InMemoryAuditCatalogChecklistItemAttestationStoreOptions, + type PostgresAuditCatalogChecklistItemAttestationStoreOptions, + resolveAuditCatalogChecklistItemAttestationFilePath, + type SqliteAuditCatalogChecklistItemAttestationStoreOptions, +} from "./catalog-checklist-item-attestation-store"; export { type ConfiguredAuditCatalogChecklistItemBlockerStoreOptions, createConfiguredAuditCatalogChecklistItemBlockerStore, @@ -199,7 +212,7 @@ export const persistencePackageBoundary = { kind: "package", phaseOwned: 2, responsibility: - "Repository contracts, checkpoint storage, database adapters, dispatch queue persistence seams, tool-history storage adapters, additive saved-audit-view adapters, additive audit-view-catalog adapters, additive catalog-visibility adapters, additive catalog-review-signal adapters, additive catalog-review-assignment adapters, additive catalog-assignment-checklist adapters, additive checklist-item-progress adapters, additive checklist-item-blocker adapters, additive checklist-item-resolution adapters, additive checklist-item-verification adapters, and additive checklist-item-evidence adapters.", + "Repository contracts, checkpoint storage, database adapters, dispatch queue persistence seams, tool-history storage adapters, additive saved-audit-view adapters, additive audit-view-catalog adapters, additive catalog-visibility adapters, additive catalog-review-signal adapters, additive catalog-review-assignment adapters, additive catalog-assignment-checklist adapters, additive checklist-item-progress adapters, additive checklist-item-blocker adapters, additive checklist-item-resolution adapters, additive checklist-item-verification adapters, additive checklist-item-evidence adapters, and additive checklist-item-attestation adapters.", publicSurface: [ "repository interfaces", "storage adapters", @@ -217,5 +230,6 @@ export const persistencePackageBoundary = { "checklist item resolution adapters", "checklist item verification adapters", "checklist item evidence adapters", + "checklist item attestation adapters", ], } as const satisfies PackageBoundary; diff --git a/packages/persistence/src/migrations.ts b/packages/persistence/src/migrations.ts index fc81490..e85de6b 100644 --- a/packages/persistence/src/migrations.ts +++ b/packages/persistence/src/migrations.ts @@ -591,6 +591,43 @@ export const runtimePersistenceMigrations = [ ON runroot_audit_catalog_checklist_item_evidence (operator_id, updated_at DESC, catalog_entry_id ASC)`, ], }, + { + id: "0015_audit_catalog_checklist_item_attestation", + postgres: [ + `CREATE TABLE IF NOT EXISTS runroot_audit_catalog_checklist_item_attestation ( + catalog_entry_id TEXT PRIMARY KEY, + kind TEXT NOT NULL, + operator_id TEXT NOT NULL, + scope_id TEXT NOT NULL, + attestation_note TEXT, + attestation_items TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + data TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_runroot_audit_catalog_checklist_item_attestation_scope + ON runroot_audit_catalog_checklist_item_attestation (scope_id, updated_at DESC, catalog_entry_id ASC)`, + `CREATE INDEX IF NOT EXISTS idx_runroot_audit_catalog_checklist_item_attestation_operator + ON runroot_audit_catalog_checklist_item_attestation (operator_id, updated_at DESC, catalog_entry_id ASC)`, + ], + sqlite: [ + `CREATE TABLE IF NOT EXISTS runroot_audit_catalog_checklist_item_attestation ( + catalog_entry_id TEXT PRIMARY KEY, + kind TEXT NOT NULL, + operator_id TEXT NOT NULL, + scope_id TEXT NOT NULL, + attestation_note TEXT, + attestation_items TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + data TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_runroot_audit_catalog_checklist_item_attestation_scope + ON runroot_audit_catalog_checklist_item_attestation (scope_id, updated_at DESC, catalog_entry_id ASC)`, + `CREATE INDEX IF NOT EXISTS idx_runroot_audit_catalog_checklist_item_attestation_operator + ON runroot_audit_catalog_checklist_item_attestation (operator_id, updated_at DESC, catalog_entry_id ASC)`, + ], + }, ] as const satisfies readonly PersistenceMigration[]; export function getRuntimePersistenceMigrationStatements( diff --git a/packages/replay/src/checklist-item-attestation.test.ts b/packages/replay/src/checklist-item-attestation.test.ts new file mode 100644 index 0000000..16dce11 --- /dev/null +++ b/packages/replay/src/checklist-item-attestation.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from "vitest"; + +import { + compareCrossRunAuditCatalogChecklistItemAttestation, + createCrossRunAuditCatalogChecklistItemAttestation, + normalizeChecklistItemAttestationItems, +} from "./checklist-item-attestation"; + +describe("@runroot/replay audit catalog checklist item attestation", () => { + it("creates checklist item attestation with normalized metadata", () => { + const attestation = createCrossRunAuditCatalogChecklistItemAttestation({ + attestationNote: " Evidence is sufficient for the queued closeout ", + catalogEntryId: " catalog_entry_attestation ", + items: [ + { + item: "Validate queued follow-up", + state: "attested", + }, + { + item: "Close backup handoff", + state: "unattested", + }, + ], + operatorId: " ops_oncall ", + scopeId: " ops ", + timestamp: "2026-04-02T04:00:00.000Z", + }); + + expect(attestation).toEqual({ + attestationNote: "Evidence is sufficient for the queued closeout", + catalogEntryId: "catalog_entry_attestation", + createdAt: "2026-04-02T04:00:00.000Z", + items: [ + { + item: "Validate queued follow-up", + state: "attested", + }, + { + item: "Close backup handoff", + state: "unattested", + }, + ], + kind: "catalog-checklist-item-attestation", + operatorId: "ops_oncall", + scopeId: "ops", + updatedAt: "2026-04-02T04:00:00.000Z", + }); + }); + + it("restricts checklist item attestation to shared evidence items", () => { + expect(() => + normalizeChecklistItemAttestationItems( + [ + { + item: "Missing item", + state: "attested", + }, + ], + ["Validate queued follow-up", "Close backup handoff"], + ), + ).toThrow( + 'Catalog checklist item attestation "Missing item" is not defined on the shared checklist item evidence layer.', + ); + }); + + it("sorts newer attestation entries ahead of older attestation entries", () => { + const olderEntry = createCrossRunAuditCatalogChecklistItemAttestation({ + catalogEntryId: "catalog_entry_older", + items: [ + { + item: "Validate queued follow-up", + state: "attested", + }, + ], + operatorId: "ops_oncall", + scopeId: "ops", + timestamp: "2026-04-02T04:00:00.000Z", + }); + const newerEntry = createCrossRunAuditCatalogChecklistItemAttestation({ + catalogEntryId: "catalog_entry_newer", + items: [ + { + item: "Validate queued follow-up", + state: "attested", + }, + ], + operatorId: "ops_oncall", + scopeId: "ops", + timestamp: "2026-04-02T04:00:01.000Z", + }); + + expect( + [olderEntry, newerEntry].sort( + compareCrossRunAuditCatalogChecklistItemAttestation, + ), + ).toEqual([newerEntry, olderEntry]); + }); +}); diff --git a/packages/replay/src/checklist-item-attestation.ts b/packages/replay/src/checklist-item-attestation.ts new file mode 100644 index 0000000..9c3dc78 --- /dev/null +++ b/packages/replay/src/checklist-item-attestation.ts @@ -0,0 +1,187 @@ +import type { + CrossRunAuditCatalogChecklistItemEvidenceApplication, + CrossRunAuditCatalogChecklistItemEvidenceView, +} from "./checklist-item-evidence"; + +export type CrossRunAuditCatalogChecklistItemAttestationState = + | "attested" + | "unattested"; + +export interface CrossRunAuditCatalogChecklistItemAttestationItem { + readonly item: string; + readonly state: CrossRunAuditCatalogChecklistItemAttestationState; +} + +export interface CrossRunAuditCatalogChecklistItemAttestation { + readonly attestationNote?: string; + readonly catalogEntryId: string; + readonly createdAt: string; + readonly items: readonly CrossRunAuditCatalogChecklistItemAttestationItem[]; + readonly kind: "catalog-checklist-item-attestation"; + readonly operatorId: string; + readonly scopeId: string; + readonly updatedAt: string; +} + +export interface CreateCrossRunAuditCatalogChecklistItemAttestationInput { + readonly attestationNote?: string; + readonly catalogEntryId: string; + readonly items: readonly CrossRunAuditCatalogChecklistItemAttestationItem[]; + readonly operatorId: string; + readonly scopeId: string; + readonly timestamp: string; +} + +export interface UpdateCrossRunAuditCatalogChecklistItemAttestationInput { + readonly attestationNote?: string; + readonly items: readonly CrossRunAuditCatalogChecklistItemAttestationItem[]; +} + +export interface CrossRunAuditCatalogChecklistItemAttestationStore { + deleteCatalogChecklistItemAttestation( + catalogEntryId: string, + ): Promise; + getCatalogChecklistItemAttestation( + catalogEntryId: string, + ): Promise; + listCatalogChecklistItemAttestations(): Promise< + readonly CrossRunAuditCatalogChecklistItemAttestation[] + >; + saveCatalogChecklistItemAttestation( + attestation: CrossRunAuditCatalogChecklistItemAttestation, + ): Promise; +} + +export interface CrossRunAuditCatalogChecklistItemAttestationView { + readonly attestation: CrossRunAuditCatalogChecklistItemAttestation; + readonly evidence: CrossRunAuditCatalogChecklistItemEvidenceView; +} + +export interface CrossRunAuditCatalogChecklistItemAttestationCollection { + readonly items: readonly CrossRunAuditCatalogChecklistItemAttestationView[]; + readonly totalCount: number; +} + +export interface CrossRunAuditCatalogChecklistItemAttestationApplication { + readonly application: CrossRunAuditCatalogChecklistItemEvidenceApplication; + readonly attestation: CrossRunAuditCatalogChecklistItemAttestationView; +} + +export function createCrossRunAuditCatalogChecklistItemAttestation( + input: CreateCrossRunAuditCatalogChecklistItemAttestationInput, +): CrossRunAuditCatalogChecklistItemAttestation { + const catalogEntryId = input.catalogEntryId.trim(); + const operatorId = input.operatorId.trim(); + const scopeId = input.scopeId.trim(); + const items = normalizeChecklistItemAttestationItems(input.items); + const attestationNote = normalizeAttestationNote(input.attestationNote); + + if (catalogEntryId.length === 0) { + throw new Error( + "Catalog checklist item attestations require a catalog entry id.", + ); + } + + if (operatorId.length === 0) { + throw new Error( + "Catalog checklist item attestations require an operator id.", + ); + } + + if (scopeId.length === 0) { + throw new Error("Catalog checklist item attestations require a scope id."); + } + + if (items.length === 0) { + throw new Error( + "Catalog checklist item attestations require at least one checklist item attestation entry.", + ); + } + + return { + ...(attestationNote ? { attestationNote } : {}), + catalogEntryId, + createdAt: input.timestamp, + items, + kind: "catalog-checklist-item-attestation", + operatorId, + scopeId, + updatedAt: input.timestamp, + }; +} + +export function compareCrossRunAuditCatalogChecklistItemAttestation( + left: CrossRunAuditCatalogChecklistItemAttestation, + right: CrossRunAuditCatalogChecklistItemAttestation, +): number { + return ( + compareAttestationPriority(left, right) || + right.updatedAt.localeCompare(left.updatedAt) || + right.createdAt.localeCompare(left.createdAt) || + left.catalogEntryId.localeCompare(right.catalogEntryId) + ); +} + +export function normalizeChecklistItemAttestationItems( + items: + | readonly CrossRunAuditCatalogChecklistItemAttestationItem[] + | undefined, + allowedItems?: readonly string[], +): readonly CrossRunAuditCatalogChecklistItemAttestationItem[] { + const allowedItemSet = allowedItems + ? new Set(allowedItems.map((item) => item.trim()).filter(Boolean)) + : undefined; + const dedupedItems = new Map< + string, + CrossRunAuditCatalogChecklistItemAttestationState + >(); + + for (const entry of items ?? []) { + const item = entry.item.trim(); + + if (item.length === 0) { + continue; + } + + if (entry.state !== "attested" && entry.state !== "unattested") { + throw new Error( + `Catalog checklist item attestations require state attested|unattested for "${item}".`, + ); + } + + if (allowedItemSet && !allowedItemSet.has(item)) { + throw new Error( + `Catalog checklist item attestation "${item}" is not defined on the shared checklist item evidence layer.`, + ); + } + + dedupedItems.set(item, entry.state); + } + + return [...dedupedItems.entries()].map(([item, state]) => ({ + item, + state, + })); +} + +function compareAttestationPriority( + left: CrossRunAuditCatalogChecklistItemAttestation, + right: CrossRunAuditCatalogChecklistItemAttestation, +): number { + const leftPriority = left.items.some((item) => item.state === "attested") + ? 0 + : 1; + const rightPriority = right.items.some((item) => item.state === "attested") + ? 0 + : 1; + + return leftPriority - rightPriority; +} + +function normalizeAttestationNote( + value: string | undefined, +): string | undefined { + const normalizedValue = value?.trim(); + + return normalizedValue ? normalizedValue : undefined; +} diff --git a/packages/replay/src/index.ts b/packages/replay/src/index.ts index 2888b83..a22d60d 100644 --- a/packages/replay/src/index.ts +++ b/packages/replay/src/index.ts @@ -34,6 +34,20 @@ export { createCrossRunAuditCatalogEntry, type PublishCrossRunAuditCatalogEntryInput, } from "./catalog"; +export { + type CreateCrossRunAuditCatalogChecklistItemAttestationInput, + type CrossRunAuditCatalogChecklistItemAttestation, + type CrossRunAuditCatalogChecklistItemAttestationApplication, + type CrossRunAuditCatalogChecklistItemAttestationCollection, + type CrossRunAuditCatalogChecklistItemAttestationItem, + type CrossRunAuditCatalogChecklistItemAttestationState, + type CrossRunAuditCatalogChecklistItemAttestationStore, + type CrossRunAuditCatalogChecklistItemAttestationView, + compareCrossRunAuditCatalogChecklistItemAttestation, + createCrossRunAuditCatalogChecklistItemAttestation, + normalizeChecklistItemAttestationItems, + type UpdateCrossRunAuditCatalogChecklistItemAttestationInput, +} from "./checklist-item-attestation"; export { type CreateCrossRunAuditCatalogChecklistItemBlockerInput, type CrossRunAuditCatalogChecklistItemBlocker, @@ -136,6 +150,7 @@ export { } from "./navigation"; export { type CrossRunAuditCatalogAssignmentChecklistQuery, + type CrossRunAuditCatalogChecklistItemAttestationQuery, type CrossRunAuditCatalogChecklistItemBlockerQuery, type CrossRunAuditCatalogChecklistItemEvidenceQuery, type CrossRunAuditCatalogChecklistItemProgressQuery, @@ -151,6 +166,7 @@ export { type CrossRunAuditReader, type CrossRunAuditSavedViewQuery, createCrossRunAuditCatalogAssignmentChecklistQuery, + createCrossRunAuditCatalogChecklistItemAttestationQuery, createCrossRunAuditCatalogChecklistItemBlockerQuery, createCrossRunAuditCatalogChecklistItemEvidenceQuery, createCrossRunAuditCatalogChecklistItemProgressQuery, diff --git a/packages/replay/src/query.ts b/packages/replay/src/query.ts index 4307fb6..532bf2f 100644 --- a/packages/replay/src/query.ts +++ b/packages/replay/src/query.ts @@ -25,6 +25,17 @@ import { createCrossRunAuditCatalogEntry, type PublishCrossRunAuditCatalogEntryInput, } from "./catalog"; +import { + type CrossRunAuditCatalogChecklistItemAttestation, + type CrossRunAuditCatalogChecklistItemAttestationApplication, + type CrossRunAuditCatalogChecklistItemAttestationCollection, + type CrossRunAuditCatalogChecklistItemAttestationStore, + type CrossRunAuditCatalogChecklistItemAttestationView, + compareCrossRunAuditCatalogChecklistItemAttestation, + createCrossRunAuditCatalogChecklistItemAttestation, + normalizeChecklistItemAttestationItems, + type UpdateCrossRunAuditCatalogChecklistItemAttestationInput, +} from "./checklist-item-attestation"; import { type CrossRunAuditCatalogChecklistItemBlocker, type CrossRunAuditCatalogChecklistItemBlockerApplication, @@ -422,6 +433,32 @@ export interface CrossRunAuditCatalogChecklistItemEvidenceQuery { ): Promise; } +export interface CrossRunAuditCatalogChecklistItemAttestationQuery { + applyCatalogEntry( + id: string, + viewer: CrossRunAuditCatalogVisibilityViewer, + ): Promise< + CrossRunAuditCatalogChecklistItemAttestationApplication | undefined + >; + clearCatalogChecklistItemAttestation( + id: string, + viewer: CrossRunAuditCatalogVisibilityViewer, + ): Promise; + getCatalogChecklistItemAttestation( + id: string, + viewer: CrossRunAuditCatalogVisibilityViewer, + ): Promise; + listAttestedCatalogEntries( + viewer: CrossRunAuditCatalogVisibilityViewer, + ): Promise; + setCatalogChecklistItemAttestation( + id: string, + viewer: CrossRunAuditCatalogVisibilityViewer, + input: UpdateCrossRunAuditCatalogChecklistItemAttestationInput, + timestamp: string, + ): Promise; +} + export function createRunTimelineQuery( reader: RunTimelineReader, ): RunTimelineQuery { @@ -1916,6 +1953,153 @@ export function createCrossRunAuditCatalogChecklistItemEvidenceQuery( }; } +export function createCrossRunAuditCatalogChecklistItemAttestationQuery( + store: CrossRunAuditCatalogChecklistItemAttestationStore, + evidenceEntries: CrossRunAuditCatalogChecklistItemEvidenceQuery, +): CrossRunAuditCatalogChecklistItemAttestationQuery { + return { + async applyCatalogEntry(id, viewer) { + const attestation = await resolveCatalogChecklistItemAttestationView( + store, + evidenceEntries, + id, + viewer, + ); + + if (!attestation) { + return undefined; + } + + const application = await evidenceEntries.applyCatalogEntry(id, viewer); + + if (!application) { + return undefined; + } + + return { + application, + attestation, + }; + }, + + async clearCatalogChecklistItemAttestation(id, viewer) { + const attestation = await resolveCatalogChecklistItemAttestationView( + store, + evidenceEntries, + id, + viewer, + ); + + if (!attestation) { + return undefined; + } + + await store.deleteCatalogChecklistItemAttestation(id); + + return attestation; + }, + + async getCatalogChecklistItemAttestation(id, viewer) { + return resolveCatalogChecklistItemAttestationView( + store, + evidenceEntries, + id, + viewer, + ); + }, + + async listAttestedCatalogEntries(viewer) { + const attestationEntries = [ + ...(await store.listCatalogChecklistItemAttestations()), + ].sort(compareCrossRunAuditCatalogChecklistItemAttestation); + const items = ( + await Promise.all( + attestationEntries.map(async (attestation) => + resolveCatalogChecklistItemAttestationFromValue( + evidenceEntries, + attestation, + viewer, + ), + ), + ) + ).filter( + ( + attestation, + ): attestation is CrossRunAuditCatalogChecklistItemAttestationView => + attestation !== undefined, + ); + + return { + items, + totalCount: items.length, + }; + }, + + async setCatalogChecklistItemAttestation(id, viewer, input, timestamp) { + const evidence = await evidenceEntries.getCatalogChecklistItemEvidence( + id, + viewer, + ); + + if (!evidence) { + throw new Error( + `Catalog entry "${id}" is not evidenced and visible to operator "${viewer.operatorId}".`, + ); + } + + const allowedItems = evidence.evidence.items.map((item) => item.item); + const normalizedItems = normalizeChecklistItemAttestationItems( + input.items, + allowedItems, + ); + + if (normalizedItems.length === 0) { + throw new Error( + "Catalog checklist item attestations require at least one checklist item attestation entry.", + ); + } + + const existingAttestation = + await store.getCatalogChecklistItemAttestation(id); + const normalizedAttestationNote = input.attestationNote?.trim(); + const attestation = existingAttestation + ? { + ...(input.attestationNote === undefined + ? existingAttestation.attestationNote + ? { attestationNote: existingAttestation.attestationNote } + : {} + : normalizedAttestationNote + ? { attestationNote: normalizedAttestationNote } + : {}), + catalogEntryId: id, + createdAt: existingAttestation.createdAt, + items: normalizedItems, + kind: existingAttestation.kind, + operatorId: viewer.operatorId, + scopeId: viewer.scopeId, + updatedAt: timestamp, + } + : createCrossRunAuditCatalogChecklistItemAttestation({ + ...(normalizedAttestationNote + ? { attestationNote: normalizedAttestationNote } + : {}), + catalogEntryId: id, + items: normalizedItems, + operatorId: viewer.operatorId, + scopeId: viewer.scopeId, + timestamp, + }); + + await store.saveCatalogChecklistItemAttestation(attestation); + + return { + attestation, + evidence, + }; + }, + }; +} + async function resolveCatalogEntryView( store: CrossRunAuditCatalogStore, savedViews: CrossRunAuditSavedViewQuery, @@ -2338,3 +2522,56 @@ async function resolveCatalogChecklistItemEvidenceFromValue( verification, }; } + +async function resolveCatalogChecklistItemAttestationView( + store: CrossRunAuditCatalogChecklistItemAttestationStore, + evidenceQuery: CrossRunAuditCatalogChecklistItemEvidenceQuery, + id: string, + viewer: CrossRunAuditCatalogVisibilityViewer, +): Promise { + const attestation = await store.getCatalogChecklistItemAttestation(id); + + return attestation + ? resolveCatalogChecklistItemAttestationFromValue( + evidenceQuery, + attestation, + viewer, + ) + : undefined; +} + +async function resolveCatalogChecklistItemAttestationFromValue( + evidenceQuery: CrossRunAuditCatalogChecklistItemEvidenceQuery, + attestation: CrossRunAuditCatalogChecklistItemAttestation, + viewer: CrossRunAuditCatalogVisibilityViewer, +): Promise { + if (attestation.scopeId !== viewer.scopeId) { + return undefined; + } + + const evidence = await evidenceQuery.getCatalogChecklistItemEvidence( + attestation.catalogEntryId, + viewer, + ); + + if (!evidence) { + return undefined; + } + + const normalizedItems = normalizeChecklistItemAttestationItems( + attestation.items, + evidence.evidence.items.map((item) => item.item), + ); + + if (normalizedItems.length === 0) { + return undefined; + } + + return { + attestation: { + ...attestation, + items: normalizedItems, + }, + evidence, + }; +} From 86e6f3e3ae087f50e1e776116158323cbd4bf48d Mon Sep 17 00:00:00 2001 From: xiwuqi Date: Thu, 2 Apr 2026 21:00:44 -0500 Subject: [PATCH 3/4] feat(phase-26): wire checklist item attestations through existing seams --- apps/api/src/server.ts | 98 +++++++ apps/web/src/app/runs/catalog/route.ts | 120 +++++++++ apps/web/src/app/runs/page.tsx | 25 ++ apps/web/src/components/console.tsx | 354 ++++++++++++++++++++++++- apps/web/src/lib/runroot-api.ts | 96 +++++++ packages/cli/src/index.ts | 86 ++++++ packages/sdk/src/errors.ts | 1 + packages/sdk/src/index.ts | 1 + packages/sdk/src/operator-service.ts | 132 +++++++++ 9 files changed, 912 insertions(+), 1 deletion(-) diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 0ed4dd0..15f6f4b 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -273,6 +273,12 @@ export function buildServer(options: BuildServerOptions = {}) { })), ); + app.get("/audit/catalog/attested", async (_request, reply) => + handleOperatorResponse(reply, async () => ({ + attested: await operator.listAttestedCatalogEntries(), + })), + ); + app.post("/audit/saved-views", async (request, reply) => handleOperatorResponse(reply, async () => { const body = request.body as { @@ -490,6 +496,22 @@ export function buildServer(options: BuildServerOptions = {}) { }), ); + app.get( + "/audit/catalog/:catalogEntryId/attestation", + async (request, reply) => + handleOperatorResponse(reply, async () => { + const params = request.params as { + readonly catalogEntryId: string; + }; + + return { + attestation: await operator.getCatalogChecklistItemAttestation( + params.catalogEntryId, + ), + }; + }), + ); + app.get("/audit/catalog/:catalogEntryId", async (request, reply) => handleOperatorResponse(reply, async () => { const params = request.params as { @@ -757,6 +779,39 @@ export function buildServer(options: BuildServerOptions = {}) { }), ); + app.post( + "/audit/catalog/:catalogEntryId/attestation", + async (request, reply) => + handleOperatorResponse(reply, async () => { + const params = request.params as { + readonly catalogEntryId: string; + }; + const body = request.body as { + readonly attestationNote?: string; + readonly items?: unknown; + }; + const items = readChecklistItemAttestationItems(body?.items, "items"); + + if (items.length === 0) { + throw new OperatorInputError( + "items must include at least one checklist item attestation entry.", + ); + } + + return { + attestation: await operator.attestCatalogEntry( + params.catalogEntryId, + { + ...(body?.attestationNote !== undefined + ? { attestationNote: body.attestationNote } + : {}), + items, + }, + ), + }; + }), + ); + app.post( "/audit/catalog/:catalogEntryId/review/clear", async (request, reply) => @@ -885,6 +940,22 @@ export function buildServer(options: BuildServerOptions = {}) { }), ); + app.post( + "/audit/catalog/:catalogEntryId/attestation/clear", + async (request, reply) => + handleOperatorResponse(reply, async () => { + const params = request.params as { + readonly catalogEntryId: string; + }; + + return { + attestation: await operator.clearCatalogChecklistItemAttestation( + params.catalogEntryId, + ), + }; + }), + ); + app.get("/audit/saved-views/:savedViewId/apply", async (request, reply) => handleOperatorResponse(reply, async () => { const params = request.params as { @@ -1398,3 +1469,30 @@ function readChecklistItemEvidenceItems( return value; } + +function readChecklistItemAttestationItems( + value: unknown, + fieldName: string, +): readonly { + readonly item: string; + readonly state: "attested" | "unattested"; +}[] { + if ( + !Array.isArray(value) || + !value.every( + (entry) => + typeof entry === "object" && + entry !== null && + "item" in entry && + typeof entry.item === "string" && + "state" in entry && + (entry.state === "attested" || entry.state === "unattested"), + ) + ) { + throw new OperatorInputError( + `${fieldName} must be an array of { item, state } objects with state attested|unattested.`, + ); + } + + return value; +} diff --git a/apps/web/src/app/runs/catalog/route.ts b/apps/web/src/app/runs/catalog/route.ts index dbdac2c..d7f2483 100644 --- a/apps/web/src/app/runs/catalog/route.ts +++ b/apps/web/src/app/runs/catalog/route.ts @@ -457,6 +457,53 @@ export async function POST(request: Request) { return Response.redirect(redirectUrl, 303); } + if (intent === "attest") { + const catalogEntryId = readTrimmedFormValue(formData, "catalogEntryId"); + const attestationItems = readChecklistItemAttestationItems( + formData.get("attestationItems"), + ); + const attestationNoteValue = formData.get("attestationNote"); + + if (!catalogEntryId) { + appendFlashMessage( + redirectUrl, + "error", + "A catalog entry id is required to update checklist item attestation metadata.", + ); + + return Response.redirect(redirectUrl, 303); + } + + if (attestationItems.length === 0) { + appendFlashMessage( + redirectUrl, + "error", + "At least one checklist item attestation entry is required.", + ); + + return Response.redirect(redirectUrl, 303); + } + + const attestation = + await createRunrootApiClient().setAuditCatalogChecklistItemAttestation( + catalogEntryId, + { + ...(typeof attestationNoteValue === "string" + ? { attestationNote: attestationNoteValue } + : {}), + items: attestationItems, + }, + ); + + appendFlashMessage( + redirectUrl, + "notice", + `Checklist item attestations for ${attestation.evidence.verification.resolution.blocker.progress.checklist.assignment.review.visibility.catalogEntry.entry.name} updated.`, + ); + + return Response.redirect(redirectUrl, 303); + } + if (intent === "clear-review") { const catalogEntryId = readTrimmedFormValue(formData, "catalogEntryId"); @@ -619,6 +666,33 @@ export async function POST(request: Request) { return Response.redirect(redirectUrl, 303); } + if (intent === "clear-attestation") { + const catalogEntryId = readTrimmedFormValue(formData, "catalogEntryId"); + + if (!catalogEntryId) { + appendFlashMessage( + redirectUrl, + "error", + "A catalog entry id is required to clear checklist item attestation metadata.", + ); + + return Response.redirect(redirectUrl, 303); + } + + const attestation = + await createRunrootApiClient().clearAuditCatalogChecklistItemAttestation( + catalogEntryId, + ); + + appendFlashMessage( + redirectUrl, + "notice", + `Checklist item attestations for ${attestation.evidence.verification.resolution.blocker.progress.checklist.assignment.review.visibility.catalogEntry.entry.name} cleared.`, + ); + + return Response.redirect(redirectUrl, 303); + } + if (intent === "clear-progress") { const catalogEntryId = readTrimmedFormValue(formData, "catalogEntryId"); @@ -976,3 +1050,49 @@ function readChecklistItemEvidenceItems( }; }); } + +function readChecklistItemAttestationItems( + value: FormDataEntryValue | null, +): readonly { + readonly item: string; + readonly state: "attested" | "unattested"; +}[] { + if (typeof value !== "string") { + return []; + } + + return value + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => { + const separatorIndex = line.indexOf(":"); + + if (separatorIndex < 0) { + return { + item: line, + state: "unattested" as const, + }; + } + + const rawState = line.slice(0, separatorIndex).trim(); + const item = line.slice(separatorIndex + 1).trim(); + + if (item.length === 0) { + throw new Error( + "Checklist item attestation lines require a non-empty item.", + ); + } + + if (rawState !== "attested" && rawState !== "unattested") { + throw new Error( + `Checklist item attestation state must be attested or unattested for "${item}".`, + ); + } + + return { + item, + state: rawState, + }; + }); +} diff --git a/apps/web/src/app/runs/page.tsx b/apps/web/src/app/runs/page.tsx index fca2ff6..5a9be54 100644 --- a/apps/web/src/app/runs/page.tsx +++ b/apps/web/src/app/runs/page.tsx @@ -3,6 +3,7 @@ import { AuditViewCatalogsView, CatalogReviewAssignmentsView, CatalogReviewSignalsView, + ChecklistItemAttestationsView, ChecklistItemBlockersView, ChecklistItemEvidencesView, ChecklistItemProgressView, @@ -22,6 +23,7 @@ import { } from "../../lib/navigation"; import { type ApiAuditCatalogAssignmentChecklistView, + type ApiAuditCatalogChecklistItemAttestationView, type ApiAuditCatalogChecklistItemBlockerView, type ApiAuditCatalogChecklistItemEvidenceView, type ApiAuditCatalogChecklistItemProgressView, @@ -62,6 +64,7 @@ export default async function RunsPage({ runs, blockedEntries, evidencedEntries, + attestedEntries, resolvedEntries, verifiedEntries, progressedEntries, @@ -75,6 +78,7 @@ export default async function RunsPage({ catalogChecklistItemBlocker, catalogChecklistItemResolution, catalogChecklistItemEvidence, + catalogChecklistItemAttestation, catalogChecklistItemVerification, catalogChecklistItemProgress, catalogAssignmentChecklist, @@ -84,6 +88,7 @@ export default async function RunsPage({ api.listRuns(), api.listBlockedAuditCatalogEntries(), api.listEvidencedAuditCatalogEntries(), + api.listAttestedAuditCatalogEntries(), api.listResolvedAuditCatalogEntries(), api.listVerifiedAuditCatalogEntries(), api.listProgressedAuditCatalogEntries(), @@ -118,6 +123,11 @@ export default async function RunsPage({ .getAuditCatalogChecklistItemEvidence(catalogEntryId) .catch(() => undefined) : Promise.resolve(undefined), + catalogEntryId + ? api + .getAuditCatalogChecklistItemAttestation(catalogEntryId) + .catch(() => undefined) + : Promise.resolve(undefined), catalogEntryId ? api .getAuditCatalogChecklistItemVerification(catalogEntryId) @@ -156,6 +166,9 @@ export default async function RunsPage({ let activeCatalogChecklistItemEvidence: | ApiAuditCatalogChecklistItemEvidenceView | undefined; + let activeCatalogChecklistItemAttestation: + | ApiAuditCatalogChecklistItemAttestationView + | undefined; let activeCatalogChecklistItemVerification: | ApiAuditCatalogChecklistItemVerificationView | undefined; @@ -174,6 +187,7 @@ export default async function RunsPage({ activeCatalogChecklistItemBlocker = catalogChecklistItemBlocker; activeCatalogChecklistItemResolution = catalogChecklistItemResolution; activeCatalogChecklistItemEvidence = catalogChecklistItemEvidence; + activeCatalogChecklistItemAttestation = catalogChecklistItemAttestation; activeCatalogChecklistItemVerification = catalogChecklistItemVerification; activeCatalogChecklistItemProgress = catalogChecklistItemProgress; activeCatalogAssignmentChecklist = catalogAssignmentChecklist; @@ -212,6 +226,12 @@ export default async function RunsPage({ ? { activeCatalogChecklistItemEvidence } : {})} /> + ) { + return ( +
+
+
+
+ Phase 26 / Checklist Item Attestations +
+

Checklist item attestations

+

+ Track thin per-item attestations and a single attestation note on + evidenced presets without turning the console into an artifact + vault, workflow engine, or collaboration product. +

+
+
+ {attestedEntries.totalCount} attested preset(s) +
+
+ + {activeCatalogChecklistItemAttestation ? ( +
+ Active attestations:{" "} + + { + activeCatalogChecklistItemAttestation.evidence.verification + .resolution.blocker.progress.checklist.assignment.review + .visibility.catalogEntry.entry.name + } + + {" · "} + {formatAttestationSummary( + activeCatalogChecklistItemAttestation.attestation.items, + )} + {activeCatalogChecklistItemAttestation.attestation.attestationNote + ? ` · ${activeCatalogChecklistItemAttestation.attestation.attestationNote}` + : ""} +
+ ) : null} + + {attestedEntries.items.length === 0 ? ( +

+ No checklist item attestations yet. Record evidence first, then save + thin attestation metadata through the shared operator seam. +

+ ) : ( +
    + {attestedEntries.items.map((attestationView) => ( +
  1. +
    +
    + + { + attestationView.evidence.verification.resolution.blocker + .progress.checklist.assignment.review.visibility + .catalogEntry.entry.name + } + +
    + {formatAttestationSummary( + attestationView.attestation.items, + )} + {" · "} + {formatEvidenceSummary( + attestationView.evidence.evidence.items, + )} + {" · "} + {formatVerificationSummary( + attestationView.evidence.verification.verification.items, + )} +
    +
    + + {formatTimestamp(attestationView.attestation.updatedAt)} + +
    +
      + {attestationView.attestation.items.map((item) => ( +
    • + {item.state}: {item.item} +
    • + ))} +
    + {attestationView.attestation.attestationNote ? ( +

    + {attestationView.attestation.attestationNote} +

    + ) : null} +
    + operator {attestationView.attestation.operatorId} · scope{" "} + {attestationView.attestation.scopeId} +
    +
    + + Apply attested preset + +
    + + + + +
    +
    + {activeCatalogChecklistItemAttestation?.evidence.verification + .resolution.blocker.progress.checklist.assignment.review + .visibility.catalogEntry.entry.id === + attestationView.evidence.verification.resolution.blocker.progress + .checklist.assignment.review.visibility.catalogEntry.entry + .id ? ( +
    Currently applied
    + ) : null} +
  2. + ))} +
+ )} +
+ ); +} + export function CatalogReviewAssignmentsView({ activeCatalogReviewAssignment, assignedEntries, @@ -1277,14 +1445,17 @@ export function CatalogReviewSignalsView({ export function AuditViewCatalogsView({ activeCatalogEntry, activeCatalogChecklistItemBlocker, + activeCatalogChecklistItemAttestation, activeCatalogChecklistItemEvidence, activeCatalogChecklistItemResolution, activeCatalogChecklistItemVerification, activeCatalogChecklistItemProgress, assignedEntries, + attestedEntries, blockedEntries, catalogEntries, checklistedEntries, + evidencedEntries, progressedEntries, resolvedEntries, verifiedEntries, @@ -1292,14 +1463,17 @@ export function AuditViewCatalogsView({ }: Readonly<{ activeCatalogEntry?: ApiAuditCatalogVisibilityView; activeCatalogChecklistItemBlocker?: ApiAuditCatalogChecklistItemBlockerView; + activeCatalogChecklistItemAttestation?: ApiAuditCatalogChecklistItemAttestationView; activeCatalogChecklistItemEvidence?: ApiAuditCatalogChecklistItemEvidenceView; activeCatalogChecklistItemResolution?: ApiAuditCatalogChecklistItemResolutionView; activeCatalogChecklistItemVerification?: ApiAuditCatalogChecklistItemVerificationView; activeCatalogChecklistItemProgress?: ApiAuditCatalogChecklistItemProgressView; assignedEntries: ApiAuditCatalogReviewAssignmentCollection; + attestedEntries: ApiAuditCatalogChecklistItemAttestationCollection; blockedEntries: ApiAuditCatalogChecklistItemBlockerCollection; catalogEntries: ApiAuditCatalogVisibilityCollection; checklistedEntries: ApiAuditCatalogAssignmentChecklistCollection; + evidencedEntries: ApiAuditCatalogChecklistItemEvidenceCollection; progressedEntries: ApiAuditCatalogChecklistItemProgressCollection; resolvedEntries: ApiAuditCatalogChecklistItemResolutionCollection; verifiedEntries: ApiAuditCatalogChecklistItemVerificationCollection; @@ -1363,6 +1537,26 @@ export function AuditViewCatalogsView({ ] as const, ), ); + const evidencesByCatalogEntryId = new Map( + evidencedEntries.items.map( + (item) => + [ + item.verification.resolution.blocker.progress.checklist.assignment + .review.visibility.catalogEntry.entry.id, + item, + ] as const, + ), + ); + const attestationsByCatalogEntryId = new Map( + attestedEntries.items.map( + (item) => + [ + item.evidence.verification.resolution.blocker.progress.checklist + .assignment.review.visibility.catalogEntry.entry.id, + item, + ] as const, + ), + ); return (
@@ -1428,7 +1622,18 @@ export function AuditViewCatalogsView({ .catalogEntry.entry.id === catalogEntry.catalogEntry.entry.id ? activeCatalogChecklistItemEvidence - : undefined; + : evidencesByCatalogEntryId.get( + catalogEntry.catalogEntry.entry.id, + ); + const attestation = + activeCatalogChecklistItemAttestation?.evidence.verification + .resolution.blocker.progress.checklist.assignment.review + .visibility.catalogEntry.entry.id === + catalogEntry.catalogEntry.entry.id + ? activeCatalogChecklistItemAttestation + : attestationsByCatalogEntryId.get( + catalogEntry.catalogEntry.entry.id, + ); const reviewSignal = reviewSignalsByCatalogEntryId.get( catalogEntry.catalogEntry.entry.id, ); @@ -1466,6 +1671,11 @@ export function AuditViewCatalogsView({ verification.verification.items, )}` : ""} + {attestation + ? ` · ${formatAttestationSummary( + attestation.attestation.items, + )}` + : ""} @@ -1559,6 +1769,20 @@ export function AuditViewCatalogsView({ evidence note: {evidence.evidence.evidenceNote}

) : null} + {attestation?.attestation.items.length ? ( +

+ attestations:{" "} + {attestation.attestation.items + .map((item) => `${item.state}: ${item.item}`) + .join(", ")} +

+ ) : null} + {attestation?.attestation.attestationNote ? ( +

+ attestation note:{" "} + {attestation.attestation.attestationNote} +

+ ) : null}
saved view {catalogEntry.catalogEntry.savedView.id} {catalogEntry.catalogEntry.savedView.refs.auditViewRunId @@ -1595,6 +1819,9 @@ export function AuditViewCatalogsView({ {evidence ? ` · evidence owner ${evidence.evidence.operatorId}` : ""} + {attestation + ? ` · attestation owner ${attestation.attestation.operatorId}` + : ""}
summary filters{" "} @@ -2031,6 +2258,61 @@ export function AuditViewCatalogsView({
) : null} + {evidence ? ( +
+ + + +
+