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) => (
+
+
+
+
+ {
+ 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}
+
+
+ {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}
+
+ ))}
+
+ )}
+
+ );
+}
+
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 ? (
+
+ ) : null}
Clear evidence
) : null}
+ {attestation ? (
+
+
+
+
+ Clear attestations
+
+ ) : null}
) : null}
+ {activeCatalogChecklistItemEvidence?.verification.resolution
+ .blocker.progress.checklist.assignment.review.visibility
+ .catalogEntry.entry.id ===
+ catalogEntry.catalogEntry.entry.id ? (
+
+ Active evidence selected
+
+ ) : null}
+ {activeCatalogChecklistItemAttestation?.evidence
+ .verification.resolution.blocker.progress.checklist
+ .assignment.review.visibility.catalogEntry.entry.id ===
+ catalogEntry.catalogEntry.entry.id ? (
+
+ Active attestations selected
+
+ ) : null}
>
);
})()}
@@ -3675,6 +3999,16 @@ function formatEvidenceSummary(
return `${referenceCount} reference(s) across ${items.length} item(s)`;
}
+function formatAttestationSummary(
+ items: readonly ApiAuditCatalogChecklistItemAttestationItem[],
+): string {
+ const attestedCount = items.filter(
+ (item) => item.state === "attested",
+ ).length;
+
+ return `${attestedCount}/${items.length} attested`;
+}
+
function formatChecklistItemProgressLines(
checklistItems: readonly string[],
progressItems:
@@ -3758,6 +4092,24 @@ function formatChecklistItemEvidenceLines(
.join("\n");
}
+function formatChecklistItemAttestationLines(
+ evidenceItems: readonly ApiAuditCatalogChecklistItemEvidenceItem[],
+ attestationItems:
+ | readonly ApiAuditCatalogChecklistItemAttestationItem[]
+ | undefined,
+): string {
+ const attestationByItem = new Map(
+ (attestationItems ?? []).map((item) => [item.item, item.state] as const),
+ );
+
+ return evidenceItems
+ .map(
+ (item) =>
+ `${attestationByItem.get(item.item) ?? "unattested"}: ${item.item}`,
+ )
+ .join("\n");
+}
+
function buildSavedAuditViewHref(savedViewId: string): string {
return `/runs?savedViewId=${encodeURIComponent(savedViewId)}`;
}
diff --git a/apps/web/src/lib/runroot-api.ts b/apps/web/src/lib/runroot-api.ts
index be5a078..167f7cf 100644
--- a/apps/web/src/lib/runroot-api.ts
+++ b/apps/web/src/lib/runroot-api.ts
@@ -558,6 +558,32 @@ export interface ApiAuditCatalogChecklistItemEvidenceCollection {
readonly totalCount: number;
}
+export interface ApiAuditCatalogChecklistItemAttestationItem {
+ readonly item: string;
+ readonly state: "attested" | "unattested";
+}
+
+export interface ApiAuditCatalogChecklistItemAttestation {
+ readonly attestationNote?: string;
+ readonly catalogEntryId: string;
+ readonly createdAt: string;
+ readonly items: readonly ApiAuditCatalogChecklistItemAttestationItem[];
+ readonly kind: "catalog-checklist-item-attestation";
+ readonly operatorId: string;
+ readonly scopeId: string;
+ readonly updatedAt: string;
+}
+
+export interface ApiAuditCatalogChecklistItemAttestationView {
+ readonly attestation: ApiAuditCatalogChecklistItemAttestation;
+ readonly evidence: ApiAuditCatalogChecklistItemEvidenceView;
+}
+
+export interface ApiAuditCatalogChecklistItemAttestationCollection {
+ readonly items: readonly ApiAuditCatalogChecklistItemAttestationView[];
+ readonly totalCount: number;
+}
+
export interface CreateApiAuditCatalogEntryInput {
readonly description?: string;
readonly name?: string;
@@ -594,6 +620,11 @@ export interface UpdateApiAuditCatalogChecklistItemEvidenceInput {
readonly items: readonly ApiAuditCatalogChecklistItemEvidenceItem[];
}
+export interface UpdateApiAuditCatalogChecklistItemAttestationInput {
+ readonly attestationNote?: string;
+ readonly items: readonly ApiAuditCatalogChecklistItemAttestationItem[];
+}
+
export interface UpdateApiAuditCatalogReviewAssignmentInput {
readonly assigneeId: string;
readonly handoffNote?: string;
@@ -642,6 +673,9 @@ export interface RunrootApiClient {
clearAuditCatalogChecklistItemEvidence(
catalogEntryId: string,
): Promise;
+ clearAuditCatalogChecklistItemAttestation(
+ catalogEntryId: string,
+ ): Promise;
clearAuditCatalogChecklistItemVerification(
catalogEntryId: string,
): Promise;
@@ -669,6 +703,9 @@ export interface RunrootApiClient {
getAuditCatalogChecklistItemEvidence(
catalogEntryId: string,
): Promise;
+ getAuditCatalogChecklistItemAttestation(
+ catalogEntryId: string,
+ ): Promise;
getAuditCatalogChecklistItemVerification(
catalogEntryId: string,
): Promise;
@@ -702,6 +739,7 @@ export interface RunrootApiClient {
listAssignedAuditCatalogEntries(): Promise;
listBlockedAuditCatalogEntries(): Promise;
listEvidencedAuditCatalogEntries(): Promise;
+ listAttestedAuditCatalogEntries(): Promise;
listResolvedAuditCatalogEntries(): Promise;
listVerifiedAuditCatalogEntries(): Promise;
listChecklistedAuditCatalogEntries(): Promise;
@@ -735,6 +773,10 @@ export interface RunrootApiClient {
catalogEntryId: string,
input: UpdateApiAuditCatalogChecklistItemEvidenceInput,
): Promise;
+ setAuditCatalogChecklistItemAttestation(
+ catalogEntryId: string,
+ input: UpdateApiAuditCatalogChecklistItemAttestationInput,
+ ): Promise;
setAuditCatalogChecklistItemVerification(
catalogEntryId: string,
input: UpdateApiAuditCatalogChecklistItemVerificationInput,
@@ -971,6 +1013,18 @@ export function createRunrootApiClient(
return payload.evidence;
},
+ async clearAuditCatalogChecklistItemAttestation(catalogEntryId) {
+ const payload = await requestJson<{
+ attestation: ApiAuditCatalogChecklistItemAttestationView;
+ }>(
+ `/audit/catalog/${catalogEntryId}/attestation/clear`,
+ { method: "POST" },
+ "web.api.clearAuditCatalogChecklistItemAttestation",
+ );
+
+ return payload.attestation;
+ },
+
async clearAuditCatalogChecklistItemProgress(catalogEntryId) {
const payload = await requestJson<{
progress: ApiAuditCatalogChecklistItemProgressView;
@@ -1079,6 +1133,18 @@ export function createRunrootApiClient(
return payload.evidence;
},
+ async getAuditCatalogChecklistItemAttestation(catalogEntryId) {
+ const payload = await requestJson<{
+ attestation: ApiAuditCatalogChecklistItemAttestationView;
+ }>(
+ `/audit/catalog/${catalogEntryId}/attestation`,
+ { method: "GET" },
+ "web.api.getAuditCatalogChecklistItemAttestation",
+ );
+
+ return payload.attestation;
+ },
+
async getAuditCatalogChecklistItemProgress(catalogEntryId) {
const payload = await requestJson<{
progress: ApiAuditCatalogChecklistItemProgressView;
@@ -1291,6 +1357,18 @@ export function createRunrootApiClient(
return payload.evidenced;
},
+ async listAttestedAuditCatalogEntries() {
+ const payload = await requestJson<{
+ attested: ApiAuditCatalogChecklistItemAttestationCollection;
+ }>(
+ "/audit/catalog/attested",
+ { method: "GET" },
+ "web.api.listAttestedAuditCatalogEntries",
+ );
+
+ return payload.attested;
+ },
+
async listVerifiedAuditCatalogEntries() {
const payload = await requestJson<{
verified: ApiAuditCatalogChecklistItemVerificationCollection;
@@ -1483,6 +1561,24 @@ export function createRunrootApiClient(
return payload.evidence;
},
+ async setAuditCatalogChecklistItemAttestation(catalogEntryId, input) {
+ const payload = await requestJson<{
+ attestation: ApiAuditCatalogChecklistItemAttestationView;
+ }>(
+ `/audit/catalog/${catalogEntryId}/attestation`,
+ {
+ body: JSON.stringify(input),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ "web.api.setAuditCatalogChecklistItemAttestation",
+ );
+
+ return payload.attestation;
+ },
+
async setAuditCatalogAssignmentChecklist(catalogEntryId, input) {
const payload = await requestJson<{
checklist: ApiAuditCatalogAssignmentChecklistView;
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index 547591d..ca03198 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -5,6 +5,7 @@ import { readFile } from "node:fs/promises";
import type { PackageBoundary } from "@runroot/config";
import type { JsonValue, RunStatus } from "@runroot/domain";
import {
+ type AttestAuditCatalogEntryInput,
type BlockAuditCatalogEntryInput,
type ChecklistAuditCatalogEntryInput,
createRunrootOperatorService,
@@ -176,6 +177,19 @@ export async function runCli(
resolveVerifyAuditCatalogEntryInput(flags),
),
});
+ case "attest":
+ if (!detail) {
+ throw new Error(
+ "audit catalog attest requires a catalog entry id.",
+ );
+ }
+
+ return writeJson(io.stdout.write, {
+ attestation: await service.attestCatalogEntry(
+ detail,
+ resolveAttestAuditCatalogEntryInput(flags),
+ ),
+ });
case "record-evidence":
if (!detail) {
throw new Error(
@@ -209,6 +223,10 @@ export async function runCli(
return writeJson(io.stdout.write, {
evidenced: await service.listEvidencedCatalogEntries(),
});
+ case "attested":
+ return writeJson(io.stdout.write, {
+ attested: await service.listAttestedCatalogEntries(),
+ });
case "checklist":
if (!detail) {
throw new Error(
@@ -278,6 +296,17 @@ export async function runCli(
return writeJson(io.stdout.write, {
evidence: await service.clearCatalogChecklistItemEvidence(detail),
});
+ case "clear-attestation":
+ if (!detail) {
+ throw new Error(
+ "audit catalog clear-attestation requires a catalog entry id.",
+ );
+ }
+
+ return writeJson(io.stdout.write, {
+ attestation:
+ await service.clearCatalogChecklistItemAttestation(detail),
+ });
case "clear-assignment":
if (!detail) {
throw new Error(
@@ -389,6 +418,17 @@ export async function runCli(
return writeJson(io.stdout.write, {
evidence: await service.getCatalogChecklistItemEvidence(detail),
});
+ case "inspect-attestation":
+ if (!detail) {
+ throw new Error(
+ "audit catalog inspect-attestation requires a catalog entry id.",
+ );
+ }
+
+ return writeJson(io.stdout.write, {
+ attestation:
+ await service.getCatalogChecklistItemAttestation(detail),
+ });
case "inspect-review":
if (!detail) {
throw new Error(
@@ -875,6 +915,22 @@ function resolveVerifyAuditCatalogEntryInput(
};
}
+function resolveAttestAuditCatalogEntryInput(
+ flags: ReadonlyMap,
+): AttestAuditCatalogEntryInput {
+ const itemsJson = getStringFlag(flags, "items-json");
+ const attestationNote = getStringFlag(flags, "attestation-note");
+
+ if (!itemsJson) {
+ throw new Error("audit catalog attest requires --items-json .");
+ }
+
+ return {
+ ...(attestationNote !== undefined ? { attestationNote } : {}),
+ items: resolveChecklistItemAttestationItems(itemsJson),
+ };
+}
+
function resolveRecordAuditCatalogEntryEvidenceInput(
flags: ReadonlyMap,
): RecordAuditCatalogEntryEvidenceInput {
@@ -1039,6 +1095,32 @@ function resolveChecklistItemEvidenceItems(itemsJson: string): readonly {
return parsedItems;
}
+function resolveChecklistItemAttestationItems(itemsJson: string): readonly {
+ readonly item: string;
+ readonly state: "attested" | "unattested";
+}[] {
+ const parsedItems = JSON.parse(itemsJson) as unknown;
+
+ if (
+ !Array.isArray(parsedItems) ||
+ !parsedItems.every(
+ (item) =>
+ typeof item === "object" &&
+ item !== null &&
+ "item" in item &&
+ typeof item.item === "string" &&
+ "state" in item &&
+ (item.state === "attested" || item.state === "unattested"),
+ )
+ ) {
+ throw new Error(
+ "--items-json must decode to an array of { item, state } objects with state attested|unattested.",
+ );
+ }
+
+ return parsedItems;
+}
+
function getStringFlag(
flags: ReadonlyMap,
name: string,
@@ -1149,6 +1231,7 @@ Commands:
audit catalog resolved
audit catalog verified
audit catalog evidenced
+ audit catalog attested
audit catalog checklisted
audit catalog progressed
audit catalog publish [--name ] [--description ]
@@ -1159,6 +1242,7 @@ Commands:
audit catalog inspect-resolution
audit catalog inspect-verification
audit catalog inspect-evidence
+ audit catalog inspect-attestation
audit catalog inspect-checklist
audit catalog inspect-progress
audit catalog inspect-review
@@ -1167,12 +1251,14 @@ Commands:
audit catalog resolve --items-json [--resolution-note ]
audit catalog verify --items-json [--verification-note ]
audit catalog record-evidence --items-json [--evidence-note ]
+ audit catalog attest --items-json [--attestation-note ]
audit catalog checklist --status [--items-json ]
audit catalog progress --items-json [--completion-note ]
audit catalog clear-blocker
audit catalog clear-resolution
audit catalog clear-verification
audit catalog clear-evidence
+ audit catalog clear-attestation
audit catalog clear-assignment
audit catalog clear-checklist
audit catalog clear-progress
diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts
index 1982bd7..b0309c4 100644
--- a/packages/sdk/src/errors.ts
+++ b/packages/sdk/src/errors.ts
@@ -30,6 +30,7 @@ export class OperatorNotFoundError extends OperatorError {
| "approval"
| "catalog assignment checklist"
| "catalog checklist item blocker"
+ | "catalog checklist item attestation"
| "catalog checklist item evidence"
| "catalog checklist item resolution"
| "catalog checklist item verification"
diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts
index 1439ceb..42e6d3a 100644
--- a/packages/sdk/src/index.ts
+++ b/packages/sdk/src/index.ts
@@ -8,6 +8,7 @@ export {
} from "./errors";
export {
type AssignAuditCatalogEntryInput,
+ type AttestAuditCatalogEntryInput,
type BlockAuditCatalogEntryInput,
type ChecklistAuditCatalogEntryInput,
createRunrootOperatorService,
diff --git a/packages/sdk/src/operator-service.ts b/packages/sdk/src/operator-service.ts
index bace3c9..29c2aed 100644
--- a/packages/sdk/src/operator-service.ts
+++ b/packages/sdk/src/operator-service.ts
@@ -19,6 +19,7 @@ import type { JsonValue, WorkflowRun } from "@runroot/domain";
import type { RunrootLogger, RunrootTracer } from "@runroot/observability";
import {
createConfiguredAuditCatalogAssignmentChecklistStore,
+ createConfiguredAuditCatalogChecklistItemAttestationStore,
createConfiguredAuditCatalogChecklistItemBlockerStore,
createConfiguredAuditCatalogChecklistItemEvidenceStore,
createConfiguredAuditCatalogChecklistItemProgressStore,
@@ -39,6 +40,10 @@ import {
type CrossRunAuditCatalogAssignmentChecklistState,
type CrossRunAuditCatalogAssignmentChecklistStore,
type CrossRunAuditCatalogAssignmentChecklistView,
+ type CrossRunAuditCatalogChecklistItemAttestationCollection,
+ type CrossRunAuditCatalogChecklistItemAttestationItem,
+ type CrossRunAuditCatalogChecklistItemAttestationStore,
+ type CrossRunAuditCatalogChecklistItemAttestationView,
type CrossRunAuditCatalogChecklistItemBlockerCollection,
type CrossRunAuditCatalogChecklistItemBlockerItem,
type CrossRunAuditCatalogChecklistItemBlockerStore,
@@ -86,6 +91,7 @@ import {
type CrossRunAuditSavedViewNavigationRefs,
type CrossRunAuditSavedViewStore,
createCrossRunAuditCatalogAssignmentChecklistQuery,
+ createCrossRunAuditCatalogChecklistItemAttestationQuery,
createCrossRunAuditCatalogChecklistItemBlockerQuery,
createCrossRunAuditCatalogChecklistItemEvidenceQuery,
createCrossRunAuditCatalogChecklistItemProgressQuery,
@@ -195,6 +201,11 @@ export interface RecordAuditCatalogEntryEvidenceInput {
readonly items: readonly CrossRunAuditCatalogChecklistItemEvidenceItem[];
}
+export interface AttestAuditCatalogEntryInput {
+ readonly attestationNote?: string;
+ readonly items: readonly CrossRunAuditCatalogChecklistItemAttestationItem[];
+}
+
export interface ReviewAuditCatalogEntryInput {
readonly note?: string;
readonly state: CrossRunAuditCatalogReviewState;
@@ -219,6 +230,10 @@ export interface RunrootOperatorService {
id: string,
input: VerifyAuditCatalogEntryInput,
): Promise;
+ attestCatalogEntry(
+ id: string,
+ input: AttestAuditCatalogEntryInput,
+ ): Promise;
recordCatalogEntryEvidence(
id: string,
input: RecordAuditCatalogEntryEvidenceInput,
@@ -237,6 +252,9 @@ export interface RunrootOperatorService {
clearCatalogChecklistItemResolution(
id: string,
): Promise;
+ clearCatalogChecklistItemAttestation(
+ id: string,
+ ): Promise;
clearCatalogChecklistItemEvidence(
id: string,
): Promise;
@@ -261,6 +279,9 @@ export interface RunrootOperatorService {
getCatalogChecklistItemResolution(
id: string,
): Promise;
+ getCatalogChecklistItemAttestation(
+ id: string,
+ ): Promise;
getCatalogChecklistItemEvidence(
id: string,
): Promise;
@@ -303,6 +324,7 @@ export interface RunrootOperatorService {
listCatalogEntries(): Promise;
listProgressedCatalogEntries(): Promise;
listResolvedCatalogEntries(): Promise;
+ listAttestedCatalogEntries(): Promise;
listEvidencedCatalogEntries(): Promise;
listVerifiedCatalogEntries(): Promise;
listReviewedCatalogEntries(): Promise;
@@ -336,6 +358,7 @@ export interface RunrootOperatorService {
export interface RunrootOperatorServiceOptions {
readonly approvalIdGenerator?: () => string;
readonly catalogAssignmentChecklistStore?: CrossRunAuditCatalogAssignmentChecklistStore;
+ readonly catalogChecklistItemAttestationStore?: CrossRunAuditCatalogChecklistItemAttestationStore;
readonly catalogChecklistItemBlockerStore?: CrossRunAuditCatalogChecklistItemBlockerStore;
readonly catalogChecklistItemEvidenceStore?: CrossRunAuditCatalogChecklistItemEvidenceStore;
readonly catalogChecklistItemResolutionStore?: CrossRunAuditCatalogChecklistItemResolutionStore;
@@ -555,6 +578,19 @@ export function createRunrootOperatorService(
? { workspacePath: options.workspacePath }
: {}),
});
+ const catalogChecklistItemAttestationStore =
+ options.catalogChecklistItemAttestationStore ??
+ createConfiguredAuditCatalogChecklistItemAttestationStore({
+ ...(options.databaseUrl ? { databaseUrl: options.databaseUrl } : {}),
+ ...(options.env ? { env: options.env } : {}),
+ ...(options.persistenceDriver
+ ? { driver: options.persistenceDriver }
+ : {}),
+ ...(options.sqlitePath ? { sqlitePath: options.sqlitePath } : {}),
+ ...(options.workspacePath
+ ? { workspacePath: options.workspacePath }
+ : {}),
+ });
const templateRuntimeOptions: CreateTemplateRuntimeBundleOptions = {
toolObserver: createToolTelemetryObserver({
history: toolHistory,
@@ -690,6 +726,11 @@ export function createRunrootOperatorService(
catalogChecklistItemEvidenceStore,
crossRunAuditCatalogChecklistItemVerifications,
);
+ const crossRunAuditCatalogChecklistItemAttestations =
+ createCrossRunAuditCatalogChecklistItemAttestationQuery(
+ catalogChecklistItemAttestationStore,
+ crossRunAuditCatalogChecklistItemEvidence,
+ );
const persistenceLocation = persistenceConfig.location;
return {
@@ -769,6 +810,19 @@ export function createRunrootOperatorService(
}
},
+ async attestCatalogEntry(id, input) {
+ try {
+ return await crossRunAuditCatalogChecklistItemAttestations.setCatalogChecklistItemAttestation(
+ id,
+ operatorIdentity,
+ input,
+ now(),
+ );
+ } catch (error) {
+ throw normalizeCatalogChecklistItemAttestationError(error, id);
+ }
+ },
+
async recordCatalogEntryEvidence(id, input) {
try {
return await crossRunAuditCatalogChecklistItemEvidence.setCatalogChecklistItemEvidence(
@@ -858,6 +912,23 @@ export function createRunrootOperatorService(
return resolution;
},
+ async clearCatalogChecklistItemAttestation(id) {
+ const attestation =
+ await crossRunAuditCatalogChecklistItemAttestations.clearCatalogChecklistItemAttestation(
+ id,
+ operatorIdentity,
+ );
+
+ if (!attestation) {
+ throw new OperatorNotFoundError(
+ "catalog checklist item attestation",
+ id,
+ );
+ }
+
+ return attestation;
+ },
+
async clearCatalogChecklistItemEvidence(id) {
const evidence =
await crossRunAuditCatalogChecklistItemEvidence.clearCatalogChecklistItemEvidence(
@@ -1030,6 +1101,23 @@ export function createRunrootOperatorService(
return resolution;
},
+ async getCatalogChecklistItemAttestation(id) {
+ const attestation =
+ await crossRunAuditCatalogChecklistItemAttestations.getCatalogChecklistItemAttestation(
+ id,
+ operatorIdentity,
+ );
+
+ if (!attestation) {
+ throw new OperatorNotFoundError(
+ "catalog checklist item attestation",
+ id,
+ );
+ }
+
+ return attestation;
+ },
+
async getCatalogChecklistItemEvidence(id) {
const evidence =
await crossRunAuditCatalogChecklistItemEvidence.getCatalogChecklistItemEvidence(
@@ -1187,6 +1275,12 @@ export function createRunrootOperatorService(
);
},
+ async listAttestedCatalogEntries() {
+ return crossRunAuditCatalogChecklistItemAttestations.listAttestedCatalogEntries(
+ operatorIdentity,
+ );
+ },
+
async listEvidencedCatalogEntries() {
return crossRunAuditCatalogChecklistItemEvidence.listEvidencedCatalogEntries(
operatorIdentity,
@@ -1792,6 +1886,44 @@ function normalizeCatalogChecklistItemEvidenceError(
return normalizeOperatorError(error);
}
+function normalizeCatalogChecklistItemAttestationError(
+ error: unknown,
+ catalogEntryId: string,
+): Error {
+ if (
+ error instanceof OperatorConflictError ||
+ error instanceof OperatorInputError ||
+ error instanceof OperatorNotFoundError
+ ) {
+ return error;
+ }
+
+ if (
+ error instanceof Error &&
+ (error.message.startsWith("Catalog checklist item attestations require") ||
+ error.message.startsWith(`Catalog checklist item attestation "`) ||
+ error.message.startsWith(
+ `Catalog entry "${catalogEntryId}" does not define checklist item evidence entries`,
+ ))
+ ) {
+ return new OperatorInputError(error.message);
+ }
+
+ if (
+ error instanceof Error &&
+ error.message.startsWith(
+ `Catalog entry "${catalogEntryId}" is not evidenced and visible to operator`,
+ )
+ ) {
+ return new OperatorNotFoundError(
+ "catalog checklist item attestation",
+ catalogEntryId,
+ );
+ }
+
+ return normalizeOperatorError(error);
+}
+
function normalizeCatalogReviewSignalError(
error: unknown,
catalogEntryId: string,
From df5d2bbcdf6d25011ffc8caed8af7f7150ce7035 Mon Sep 17 00:00:00 2001
From: xiwuqi
Date: Thu, 2 Apr 2026 21:00:56 -0500
Subject: [PATCH 4/4] test(phase-26): cover inline and queued checklist item
attestation paths
---
apps/api/src/server.integration.test.ts | 161 ++++++++-------
apps/api/src/server.test.ts | 2 +-
apps/web/src/app/web.integration.test.tsx | 57 ++++--
packages/cli/src/index.integration.test.ts | 191 +++++++++++-------
.../src/operator-service.integration.test.ts | 77 ++++---
5 files changed, 302 insertions(+), 186 deletions(-)
diff --git a/apps/api/src/server.integration.test.ts b/apps/api/src/server.integration.test.ts
index f71d133..fd6ed94 100644
--- a/apps/api/src/server.integration.test.ts
+++ b/apps/api/src/server.integration.test.ts
@@ -3373,7 +3373,7 @@ describe("@runroot/api integration", () => {
}
});
- it("serves record-evidence, list-evidenced, inspect-evidence, clear-evidence, and apply paths through the network API", async () => {
+ it("serves attest, list-attested, inspect-attestation, clear-attestation, and apply paths through the network API", async () => {
const workspaceRoot = await mkdtemp(
join(tmpdir(), "runroot-api-checklist-evidence-"),
);
@@ -3644,62 +3644,86 @@ describe("@runroot/api integration", () => {
method: "POST",
},
);
- const evidencedResponse = await fetch(
- `${peerAddress}/audit/catalog/evidenced`,
+ const attestationResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/attestation`,
+ {
+ body: JSON.stringify({
+ attestationNote: "Backup attested the stable follow-up evidence",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "attested",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unattested",
+ },
+ ],
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ );
+ const attestedResponse = await fetch(
+ `${peerAddress}/audit/catalog/attested`,
);
const inspectResponse = await fetch(
- `${peerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/evidence`,
+ `${peerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/attestation`,
);
const applyResponse = await fetch(
`${peerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/apply`,
);
const clearResponse = await fetch(
- `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/evidence/clear`,
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/attestation/clear`,
{
method: "POST",
},
);
- const evidencedAfterClearResponse = await fetch(
- `${peerAddress}/audit/catalog/evidenced`,
+ const attestedAfterClearResponse = await fetch(
+ `${peerAddress}/audit/catalog/attested`,
);
- const evidencePayload = (await evidenceResponse.json()) as {
- evidence: {
- verification: {
- resolution: {
- resolution: {
- resolutionNote?: string;
- };
- };
- verification: {
- verificationNote?: string;
- };
- };
- evidence: {
- evidenceNote?: string;
+ const attestationPayload = (await attestationResponse.json()) as {
+ attestation: {
+ attestation: {
+ attestationNote?: string;
items: Array<{
item: string;
- references: string[];
+ state: "attested" | "unattested";
}>;
};
- };
- };
- const evidencedPayload = (await evidencedResponse.json()) as {
- evidenced: {
- items: Array<{
+ evidence: {
evidence: {
evidenceNote?: string;
};
verification: {
- resolution: {
- blocker: {
- progress: {
- checklist: {
- assignment: {
- review: {
- visibility: {
- catalogEntry: {
- entry: {
- id: string;
+ verification: {
+ verificationNote?: string;
+ };
+ };
+ };
+ };
+ };
+ const attestedPayload = (await attestedResponse.json()) as {
+ attested: {
+ items: Array<{
+ attestation: {
+ attestationNote?: string;
+ };
+ evidence: {
+ verification: {
+ resolution: {
+ blocker: {
+ progress: {
+ checklist: {
+ assignment: {
+ review: {
+ visibility: {
+ catalogEntry: {
+ entry: {
+ id: string;
+ };
};
};
};
@@ -3715,12 +3739,12 @@ describe("@runroot/api integration", () => {
};
};
const inspectPayload = (await inspectResponse.json()) as {
- evidence: {
- evidence: {
- evidenceNote?: string;
+ attestation: {
+ attestation: {
+ attestationNote?: string;
items: Array<{
item: string;
- references: string[];
+ state: "attested" | "unattested";
}>;
};
};
@@ -3743,15 +3767,15 @@ describe("@runroot/api integration", () => {
};
};
const clearPayload = (await clearResponse.json()) as {
- evidence: {
- evidence: {
- evidenceNote?: string;
+ attestation: {
+ attestation: {
+ attestationNote?: string;
};
};
};
- const evidencedAfterClearPayload =
- (await evidencedAfterClearResponse.json()) as {
- evidenced: {
+ const attestedAfterClearPayload =
+ (await attestedAfterClearResponse.json()) as {
+ attested: {
totalCount: number;
};
};
@@ -3767,36 +3791,37 @@ describe("@runroot/api integration", () => {
expect(resolutionResponse.status).toBe(200);
expect(verificationResponse.status).toBe(200);
expect(evidenceResponse.status).toBe(200);
+ expect(attestationResponse.status).toBe(200);
expect(
- evidencePayload.evidence.verification.resolution.resolution
- .resolutionNote,
- ).toBe("Backup confirmed the follow-up closure");
- expect(
- evidencePayload.evidence.verification.verification.verificationNote,
+ attestationPayload.attestation.evidence.verification.verification
+ .verificationNote,
).toBe("Backup verified the follow-up closure");
- expect(evidencePayload.evidence.evidence.evidenceNote).toBe(
- "Backup collected stable follow-up references",
+ expect(
+ attestationPayload.attestation.evidence.evidence.evidenceNote,
+ ).toBe("Backup collected stable follow-up references");
+ expect(attestationPayload.attestation.attestation.attestationNote).toBe(
+ "Backup attested the stable follow-up evidence",
);
- expect(evidencePayload.evidence.evidence.items).toEqual([
+ expect(attestationPayload.attestation.attestation.items).toEqual([
{
item: "Validate queued follow-up",
- references: ["run://queued-follow-up", "note://backup-closeout"],
+ state: "attested",
},
{
item: "Close backup handoff",
- references: ["doc://backup-handoff"],
+ state: "unattested",
},
]);
- expect(evidencedResponse.status).toBe(200);
- expect(evidencedPayload.evidenced.totalCount).toBe(1);
+ expect(attestedResponse.status).toBe(200);
+ expect(attestedPayload.attested.totalCount).toBe(1);
expect(
- evidencedPayload.evidenced.items[0]?.verification.resolution.blocker
- .progress.checklist.assignment.review.visibility.catalogEntry.entry
- .id,
+ attestedPayload.attested.items[0]?.evidence.verification.resolution
+ .blocker.progress.checklist.assignment.review.visibility.catalogEntry
+ .entry.id,
).toBe("catalog_entry_evidence_api");
expect(inspectResponse.status).toBe(200);
- expect(inspectPayload.evidence.evidence.evidenceNote).toBe(
- "Backup collected stable follow-up references",
+ expect(inspectPayload.attestation.attestation.attestationNote).toBe(
+ "Backup attested the stable follow-up evidence",
);
expect(applyResponse.status).toBe(200);
expect(applyPayload.application.application.savedView.id).toBe(
@@ -3807,11 +3832,11 @@ describe("@runroot/api integration", () => {
.runId,
).toBe(queuedRun.id);
expect(clearResponse.status).toBe(200);
- expect(clearPayload.evidence.evidence.evidenceNote).toBe(
- "Backup collected stable follow-up references",
+ expect(clearPayload.attestation.attestation.attestationNote).toBe(
+ "Backup attested the stable follow-up evidence",
);
- expect(evidencedAfterClearResponse.status).toBe(200);
- expect(evidencedAfterClearPayload.evidenced.totalCount).toBe(0);
+ expect(attestedAfterClearResponse.status).toBe(200);
+ expect(attestedAfterClearPayload.attested.totalCount).toBe(0);
} finally {
await ownerApp.close();
await peerApp.close();
diff --git a/apps/api/src/server.test.ts b/apps/api/src/server.test.ts
index 12da1ab..eb0f13e 100644
--- a/apps/api/src/server.test.ts
+++ b/apps/api/src/server.test.ts
@@ -25,7 +25,7 @@ describe("@runroot/api", () => {
expect(response.json()).toEqual({
status: "ok",
project: "Runroot",
- phase: 25,
+ phase: 26,
});
});
diff --git a/apps/web/src/app/web.integration.test.tsx b/apps/web/src/app/web.integration.test.tsx
index e407189..fa7ddbc 100644
--- a/apps/web/src/app/web.integration.test.tsx
+++ b/apps/web/src/app/web.integration.test.tsx
@@ -2338,7 +2338,7 @@ describe("@runroot/web integration", () => {
}
});
- it("renders, records, clears, and reapplies checklist item evidence through the existing API surface", async () => {
+ it("renders, records, clears, and reapplies checklist item attestations through the existing API surface", async () => {
const workspaceRoot = await mkdtemp(
join(tmpdir(), "runroot-web-checklist-evidence-"),
);
@@ -2638,9 +2638,34 @@ describe("@runroot/web integration", () => {
expect(evidenceResponse.status).toBe(303);
+ const attestationForm = new FormData();
+ attestationForm.set("catalogEntryId", "catalog_entry_evidence_web");
+ attestationForm.set("intent", "attest");
+ attestationForm.set(
+ "attestationItems",
+ "attested: Validate queued follow-up\nunattested: Close backup handoff",
+ );
+ attestationForm.set(
+ "attestationNote",
+ "Backup attested the stable follow-up evidence",
+ );
+ attestationForm.set(
+ "returnTo",
+ "/runs?catalogEntryId=catalog_entry_evidence_web",
+ );
+
+ const attestationResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: attestationForm,
+ method: "POST",
+ }),
+ );
+
+ expect(attestationResponse.status).toBe(303);
+
process.env.RUNROOT_API_BASE_URL = peerAddress;
- const evidencedMarkup = renderToStaticMarkup(
+ const attestedMarkup = renderToStaticMarkup(
await RunsPage({
searchParams: Promise.resolve({
catalogEntryId: "catalog_entry_evidence_web",
@@ -2648,25 +2673,25 @@ describe("@runroot/web integration", () => {
}),
);
- expect(evidencedMarkup).toContain("Checklist item evidence");
- expect(evidencedMarkup).toContain("Queued evidence preset");
- expect(evidencedMarkup).toContain("3 reference(s) across 2 item(s)");
- expect(evidencedMarkup).toContain("Validate queued follow-up");
- expect(evidencedMarkup).toContain("Close backup handoff");
- expect(evidencedMarkup).toContain(
- "Backup collected stable follow-up references",
+ expect(attestedMarkup).toContain("Checklist item attestations");
+ expect(attestedMarkup).toContain("Queued evidence preset");
+ expect(attestedMarkup).toContain("1/2 attested");
+ expect(attestedMarkup).toContain("Validate queued follow-up");
+ expect(attestedMarkup).toContain("Close backup handoff");
+ expect(attestedMarkup).toContain(
+ "Backup attested the stable follow-up evidence",
);
- expect(evidencedMarkup).toContain("Apply evidenced preset");
- expect(evidencedMarkup).toContain(
+ expect(attestedMarkup).toContain("Apply attested preset");
+ expect(attestedMarkup).toContain(
`Queued worker ${queuedRun.id} handed to backup`,
);
- expect(evidencedMarkup).toContain("Currently applied");
+ expect(attestedMarkup).toContain("Active attestations selected");
process.env.RUNROOT_API_BASE_URL = ownerAddress;
const clearForm = new FormData();
clearForm.set("catalogEntryId", "catalog_entry_evidence_web");
- clearForm.set("intent", "clear-evidence");
+ clearForm.set("intent", "clear-attestation");
clearForm.set(
"returnTo",
"/runs?catalogEntryId=catalog_entry_evidence_web",
@@ -2691,10 +2716,10 @@ describe("@runroot/web integration", () => {
}),
);
- expect(clearedMarkup).toContain("Checklist item evidence");
- expect(clearedMarkup).toContain("No checklist item evidence yet");
+ expect(clearedMarkup).toContain("Checklist item attestations");
+ expect(clearedMarkup).toContain("No checklist item attestations yet");
expect(clearedMarkup).not.toContain(
- "Backup collected stable follow-up references",
+ "Backup attested the stable follow-up evidence",
);
} finally {
await ownerApp.close();
diff --git a/packages/cli/src/index.integration.test.ts b/packages/cli/src/index.integration.test.ts
index f9c6f1b..946e666 100644
--- a/packages/cli/src/index.integration.test.ts
+++ b/packages/cli/src/index.integration.test.ts
@@ -4355,7 +4355,7 @@ describe("@runroot/cli integration", () => {
expect(verifiedAfterClearPayload.verified.totalCount).toBe(0);
});
- it("records evidence, lists-evidenced, inspects, clears, and reapplies verified catalog entries through the CLI", async () => {
+ it("records attestations, lists-attested, inspects, clears, and reapplies evidenced catalog entries through the CLI", async () => {
const workspaceRoot = await mkdtemp(
join(tmpdir(), "runroot-cli-checklist-evidence-"),
);
@@ -4382,11 +4382,12 @@ describe("@runroot/cli integration", () => {
const resolutionIo = createIo();
const verificationIo = createIo();
const evidenceIo = createIo();
- const evidencedPeerIo = createIo();
- const inspectEvidenceIo = createIo();
+ const attestationIo = createIo();
+ const attestedPeerIo = createIo();
+ const inspectAttestationIo = createIo();
const applyIo = createIo();
- const clearEvidenceIo = createIo();
- const evidencedAfterClearIo = createIo();
+ const clearAttestationIo = createIo();
+ const attestedAfterClearIo = createIo();
await runCli(
["runs", "start", "shell-runbook-flow", "--input-file", inputFile],
@@ -4723,8 +4724,39 @@ describe("@runroot/cli integration", () => {
io: evidenceIo.io,
},
);
- const evidencedPeerExitCode = await runCli(
- ["audit", "catalog", "evidenced"],
+ const attestationExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "attest",
+ publishedPayload.catalogEntry.entry.id,
+ "--items-json",
+ JSON.stringify([
+ {
+ item: "Validate queued follow-up",
+ state: "attested",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unattested",
+ },
+ ]),
+ "--attestation-note",
+ "Backup attested the stable follow-up evidence",
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: attestationIo.io,
+ },
+ );
+ const attestedPeerExitCode = await runCli(
+ ["audit", "catalog", "attested"],
{
cwd: workspaceRoot,
env: {
@@ -4733,14 +4765,14 @@ describe("@runroot/cli integration", () => {
RUNROOT_PERSISTENCE_DRIVER: "sqlite",
RUNROOT_SQLITE_PATH: sqlitePath,
},
- io: evidencedPeerIo.io,
+ io: attestedPeerIo.io,
},
);
- const inspectEvidenceExitCode = await runCli(
+ const inspectAttestationExitCode = await runCli(
[
"audit",
"catalog",
- "inspect-evidence",
+ "inspect-attestation",
publishedPayload.catalogEntry.entry.id,
],
{
@@ -4751,7 +4783,7 @@ describe("@runroot/cli integration", () => {
RUNROOT_PERSISTENCE_DRIVER: "sqlite",
RUNROOT_SQLITE_PATH: sqlitePath,
},
- io: inspectEvidenceIo.io,
+ io: inspectAttestationIo.io,
},
);
const applyExitCode = await runCli(
@@ -4767,11 +4799,11 @@ describe("@runroot/cli integration", () => {
io: applyIo.io,
},
);
- const clearEvidenceExitCode = await runCli(
+ const clearAttestationExitCode = await runCli(
[
"audit",
"catalog",
- "clear-evidence",
+ "clear-attestation",
publishedPayload.catalogEntry.entry.id,
],
{
@@ -4782,11 +4814,11 @@ describe("@runroot/cli integration", () => {
RUNROOT_PERSISTENCE_DRIVER: "sqlite",
RUNROOT_SQLITE_PATH: sqlitePath,
},
- io: clearEvidenceIo.io,
+ io: clearAttestationIo.io,
},
);
- const evidencedAfterClearExitCode = await runCli(
- ["audit", "catalog", "evidenced"],
+ const attestedAfterClearExitCode = await runCli(
+ ["audit", "catalog", "attested"],
{
cwd: workspaceRoot,
env: {
@@ -4795,44 +4827,49 @@ describe("@runroot/cli integration", () => {
RUNROOT_PERSISTENCE_DRIVER: "sqlite",
RUNROOT_SQLITE_PATH: sqlitePath,
},
- io: evidencedAfterClearIo.io,
+ io: attestedAfterClearIo.io,
},
);
- const evidencePayload = JSON.parse(evidenceIo.stdout.join("")) as {
- evidence: {
- evidence: {
- evidenceNote?: string;
+ const attestationPayload = JSON.parse(attestationIo.stdout.join("")) as {
+ attestation: {
+ attestation: {
+ attestationNote?: string;
items: Array<{
item: string;
- references: string[];
+ state: "attested" | "unattested";
}>;
};
- verification: {
+ evidence: {
+ evidence: {
+ evidenceNote?: string;
+ };
verification: {
- verificationNote?: string;
+ verification: {
+ verificationNote?: string;
+ };
};
};
};
};
- const evidencedPeerPayload = JSON.parse(
- evidencedPeerIo.stdout.join(""),
- ) as {
- evidenced: {
+ const attestedPeerPayload = JSON.parse(attestedPeerIo.stdout.join("")) as {
+ attested: {
items: Array<{
- evidence: {
- evidenceNote?: string;
+ attestation: {
+ attestationNote?: string;
};
- verification: {
- resolution: {
- blocker: {
- progress: {
- checklist: {
- assignment: {
- review: {
- visibility: {
- catalogEntry: {
- entry: {
- id: string;
+ evidence: {
+ verification: {
+ resolution: {
+ blocker: {
+ progress: {
+ checklist: {
+ assignment: {
+ review: {
+ visibility: {
+ catalogEntry: {
+ entry: {
+ id: string;
+ };
};
};
};
@@ -4847,15 +4884,15 @@ describe("@runroot/cli integration", () => {
totalCount: number;
};
};
- const inspectEvidencePayload = JSON.parse(
- inspectEvidenceIo.stdout.join(""),
+ const inspectAttestationPayload = JSON.parse(
+ inspectAttestationIo.stdout.join(""),
) as {
- evidence: {
- evidence: {
- evidenceNote?: string;
+ attestation: {
+ attestation: {
+ attestationNote?: string;
items: Array<{
item: string;
- references: string[];
+ state: "attested" | "unattested";
}>;
};
};
@@ -4877,19 +4914,19 @@ describe("@runroot/cli integration", () => {
};
};
};
- const clearEvidencePayload = JSON.parse(
- clearEvidenceIo.stdout.join(""),
+ const clearAttestationPayload = JSON.parse(
+ clearAttestationIo.stdout.join(""),
) as {
- evidence: {
- evidence: {
- evidenceNote?: string;
+ attestation: {
+ attestation: {
+ attestationNote?: string;
};
};
};
- const evidencedAfterClearPayload = JSON.parse(
- evidencedAfterClearIo.stdout.join(""),
+ const attestedAfterClearPayload = JSON.parse(
+ attestedAfterClearIo.stdout.join(""),
) as {
- evidenced: {
+ attested: {
totalCount: number;
};
};
@@ -4904,35 +4941,41 @@ describe("@runroot/cli integration", () => {
expect(resolutionExitCode).toBe(0);
expect(verificationExitCode).toBe(0);
expect(evidenceExitCode).toBe(0);
- expect(evidencedPeerExitCode).toBe(0);
- expect(inspectEvidenceExitCode).toBe(0);
+ expect(attestationExitCode).toBe(0);
+ expect(attestedPeerExitCode).toBe(0);
+ expect(inspectAttestationExitCode).toBe(0);
expect(applyExitCode).toBe(0);
- expect(clearEvidenceExitCode).toBe(0);
- expect(evidencedAfterClearExitCode).toBe(0);
+ expect(clearAttestationExitCode).toBe(0);
+ expect(attestedAfterClearExitCode).toBe(0);
expect(
- evidencePayload.evidence.verification.verification.verificationNote,
+ attestationPayload.attestation.evidence.verification.verification
+ .verificationNote,
).toBe("Backup verified the follow-up closure");
- expect(evidencePayload.evidence.evidence.evidenceNote).toBe(
+ expect(attestationPayload.attestation.evidence.evidence.evidenceNote).toBe(
"Backup collected stable follow-up references",
);
- expect(evidencePayload.evidence.evidence.items).toEqual([
+ expect(attestationPayload.attestation.attestation.attestationNote).toBe(
+ "Backup attested the stable follow-up evidence",
+ );
+ expect(attestationPayload.attestation.attestation.items).toEqual([
{
item: "Validate queued follow-up",
- references: ["run://queued-follow-up", "note://backup-closeout"],
+ state: "attested",
},
{
item: "Close backup handoff",
- references: ["doc://backup-handoff"],
+ state: "unattested",
},
]);
- expect(evidencedPeerPayload.evidenced.totalCount).toBe(1);
+ expect(attestedPeerPayload.attested.totalCount).toBe(1);
expect(
- evidencedPeerPayload.evidenced.items[0]?.verification.resolution.blocker
- .progress.checklist.assignment.review.visibility.catalogEntry.entry.id,
+ attestedPeerPayload.attested.items[0]?.evidence.verification.resolution
+ .blocker.progress.checklist.assignment.review.visibility.catalogEntry
+ .entry.id,
).toBe(publishedPayload.catalogEntry.entry.id);
- expect(inspectEvidencePayload.evidence.evidence.evidenceNote).toBe(
- "Backup collected stable follow-up references",
- );
+ expect(
+ inspectAttestationPayload.attestation.attestation.attestationNote,
+ ).toBe("Backup attested the stable follow-up evidence");
expect(applyPayload.application.application.savedView.id).toBe(
savedViewPayload.savedView.id,
);
@@ -4940,9 +4983,9 @@ describe("@runroot/cli integration", () => {
applyPayload.application.application.navigation.drilldowns[0]?.result
.runId,
).toBe(queuedRun.run.id);
- expect(clearEvidencePayload.evidence.evidence.evidenceNote).toBe(
- "Backup collected stable follow-up references",
- );
- expect(evidencedAfterClearPayload.evidenced.totalCount).toBe(0);
+ expect(
+ clearAttestationPayload.attestation.attestation.attestationNote,
+ ).toBe("Backup attested the stable follow-up evidence");
+ expect(attestedAfterClearPayload.attested.totalCount).toBe(0);
});
});
diff --git a/packages/sdk/src/operator-service.integration.test.ts b/packages/sdk/src/operator-service.integration.test.ts
index aa9ff3d..5908b94 100644
--- a/packages/sdk/src/operator-service.integration.test.ts
+++ b/packages/sdk/src/operator-service.integration.test.ts
@@ -2273,7 +2273,7 @@ describe("@runroot/sdk operator service integration", () => {
expect(peerVerificationsAfterClear.totalCount).toBe(0);
});
- it("records evidence, lists-evidenced, inspects, clears, and reapplies verified presets for inline and queued runs through the operator seam", async () => {
+ it("records attestations, lists-attested, inspects, clears, and reapplies evidenced presets for inline and queued runs through the operator seam", async () => {
const workspaceRoot = await mkdtemp(
join(tmpdir(), "runroot-sdk-checklist-evidence-"),
);
@@ -2511,64 +2511,87 @@ describe("@runroot/sdk operator service integration", () => {
},
],
});
+ await ownerService.attestCatalogEntry(inlineCatalogEntry.entry.id, {
+ items: [
+ {
+ item: "Confirm inline owner follow-up",
+ state: "attested",
+ },
+ ],
+ });
+ await ownerService.attestCatalogEntry(queuedCatalogEntry.entry.id, {
+ attestationNote: "Backup attested the stable follow-up evidence",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "attested",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unattested",
+ },
+ ],
+ });
- const ownerEvidence = await ownerService.listEvidencedCatalogEntries();
- const peerEvidence = await peerService.listEvidencedCatalogEntries();
- const inspectedEvidence =
- await ownerService.getCatalogChecklistItemEvidence(
+ const ownerAttestations = await ownerService.listAttestedCatalogEntries();
+ const peerAttestations = await peerService.listAttestedCatalogEntries();
+ const inspectedAttestation =
+ await ownerService.getCatalogChecklistItemAttestation(
queuedCatalogEntry.entry.id,
);
- const appliedEvidence = await peerService.applyCatalogEntry(
+ const appliedAttestation = await peerService.applyCatalogEntry(
queuedCatalogEntry.entry.id,
);
- const clearedEvidence =
- await ownerService.clearCatalogChecklistItemEvidence(
+ const clearedAttestation =
+ await ownerService.clearCatalogChecklistItemAttestation(
queuedCatalogEntry.entry.id,
);
- const peerEvidenceAfterClear =
- await peerService.listEvidencedCatalogEntries();
+ const peerAttestationsAfterClear =
+ await peerService.listAttestedCatalogEntries();
expect(
- ownerEvidence.items.map(
+ ownerAttestations.items.map(
(item) =>
- item.verification.resolution.blocker.progress.checklist.assignment
- .review.visibility.catalogEntry.entry.id,
+ item.evidence.verification.resolution.blocker.progress.checklist
+ .assignment.review.visibility.catalogEntry.entry.id,
),
).toEqual([queuedCatalogEntry.entry.id, inlineCatalogEntry.entry.id]);
expect(
- peerEvidence.items.map(
+ peerAttestations.items.map(
(item) =>
- item.verification.resolution.blocker.progress.checklist.assignment
- .review.visibility.catalogEntry.entry.id,
+ item.evidence.verification.resolution.blocker.progress.checklist
+ .assignment.review.visibility.catalogEntry.entry.id,
),
).toEqual([queuedCatalogEntry.entry.id]);
- expect(inspectedEvidence.evidence).toMatchObject({
+ expect(inspectedAttestation.attestation).toMatchObject({
catalogEntryId: queuedCatalogEntry.entry.id,
- evidenceNote: "Backup collected stable follow-up references",
+ attestationNote: "Backup attested the stable follow-up evidence",
operatorId: "ops_oncall",
scopeId: "ops",
});
- expect(inspectedEvidence.evidence.items).toEqual([
+ expect(inspectedAttestation.attestation.items).toEqual([
{
item: "Validate queued follow-up",
- references: ["run://queued-follow-up", "note://backup-closeout"],
+ state: "attested",
},
{
item: "Close backup handoff",
- references: ["doc://backup-handoff"],
+ state: "unattested",
},
]);
- expect(appliedEvidence.catalogEntry.entry.id).toBe(
+ expect(appliedAttestation.catalogEntry.entry.id).toBe(
queuedCatalogEntry.entry.id,
);
- expect(appliedEvidence.application.savedView.id).toBe(queuedSavedView.id);
- expect(appliedEvidence.application.navigation.totalSummaryCount).toBe(1);
+ expect(appliedAttestation.application.savedView.id).toBe(
+ queuedSavedView.id,
+ );
+ expect(appliedAttestation.application.navigation.totalSummaryCount).toBe(1);
expect(
- appliedEvidence.application.navigation.drilldowns[0]?.result.runId,
+ appliedAttestation.application.navigation.drilldowns[0]?.result.runId,
).toBe(queuedRun.id);
- expect(clearedEvidence.evidence.catalogEntryId).toBe(
+ expect(clearedAttestation.attestation.catalogEntryId).toBe(
queuedCatalogEntry.entry.id,
);
- expect(peerEvidenceAfterClear.totalCount).toBe(0);
+ expect(peerAttestationsAfterClear.totalCount).toBe(0);
});
});