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/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 } : {})} /> + { } }); - 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/apps/web/src/components/console.tsx b/apps/web/src/components/console.tsx index a03228e..844dc20 100644 --- a/apps/web/src/components/console.tsx +++ b/apps/web/src/components/console.tsx @@ -4,6 +4,9 @@ import type { ApiApproval, ApiAuditCatalogAssignmentChecklistCollection, ApiAuditCatalogAssignmentChecklistView, + ApiAuditCatalogChecklistItemAttestationCollection, + ApiAuditCatalogChecklistItemAttestationItem, + ApiAuditCatalogChecklistItemAttestationView, ApiAuditCatalogChecklistItemBlockerCollection, ApiAuditCatalogChecklistItemBlockerItem, ApiAuditCatalogChecklistItemBlockerView, @@ -1042,6 +1045,171 @@ export function ChecklistItemEvidencesView({ ); } +export function ChecklistItemAttestationsView({ + activeCatalogChecklistItemAttestation, + attestedEntries, +}: Readonly<{ + activeCatalogChecklistItemAttestation?: ApiAuditCatalogChecklistItemAttestationView; + attestedEntries: ApiAuditCatalogChecklistItemAttestationCollection; +}>) { + return ( +
+
+
+
+ Phase 26 / Checklist Item Attestations +
+

Checklist item attestations

+

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

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

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

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

    + {attestationView.attestation.attestationNote} +

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

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

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

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

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

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