@agent-delivery-harness/cli 0.2.0 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +104 -201
  2. package/NOTICE +1 -1
  3. package/package.json +5 -5
  4. package/src/boundary.ts +84 -63
  5. package/src/check-snapshot.ts +167 -0
  6. package/src/command-activity.ts +75 -0
  7. package/src/commands/admit.ts +106 -0
  8. package/src/commands/check.ts +13 -2
  9. package/src/commands/emit-review-evidence.ts +38 -0
  10. package/src/commands/emit.ts +80 -12
  11. package/src/commands/gate.ts +64 -13
  12. package/src/commands/maintain.ts +1 -0
  13. package/src/commands/managed.ts +105 -34
  14. package/src/commands/prepare.ts +148 -22
  15. package/src/commands/record.ts +107 -3
  16. package/src/commands/resume.ts +53 -0
  17. package/src/commands/review-context.ts +19 -0
  18. package/src/commands/runs.ts +461 -30
  19. package/src/commands/save-context.ts +51 -0
  20. package/src/commands/submit-evidence.ts +46 -7
  21. package/src/commands/verify.ts +156 -10
  22. package/src/declared-checks.ts +50 -0
  23. package/src/index.ts +17 -1
  24. package/src/main.ts +56 -65
  25. package/src/ordinary-context.ts +83 -0
  26. package/src/provider-rails.ts +23 -704
  27. package/src/record-retention.ts +325 -0
  28. package/src/review-evidence.ts +284 -0
  29. package/src/run-archive-commands.ts +164 -0
  30. package/src/run-archive.ts +127 -0
  31. package/src/run-artifact-commands.ts +163 -0
  32. package/src/run-attachments.ts +112 -0
  33. package/src/run-export.ts +202 -0
  34. package/src/run-live.ts +102 -0
  35. package/src/run-projection.ts +327 -12
  36. package/src/run-server.ts +403 -149
  37. package/src/run-surface.ts +196 -9
  38. package/src/run-view-command.ts +77 -0
  39. package/src/run-view-html.ts +138 -0
  40. package/src/run-view-record.ts +119 -0
  41. package/src/run-view.ts +343 -0
  42. package/src/scoped-attempts.ts +86 -0
  43. package/src/scoped-candidate.ts +28 -0
  44. package/src/scoped-checks.ts +196 -0
  45. package/src/scoped-diagnostics.ts +124 -0
  46. package/src/scoped-observations.ts +51 -0
  47. package/src/snapshot-inventory-worker.ts +40 -0
@@ -3,6 +3,28 @@
3
3
  * found, how an event envelope is built, and how a store-derived string is
4
4
  * made safe to print.
5
5
  *
6
+ * THE ONE STORE OVERRIDE. `DELIVERY_HARNESS_RUN_STORE` names an absolute
7
+ * directory to resolve the store under instead of the repository's git common
8
+ * directory, read HERE — at resolution time, on every CLI path that reaches a
9
+ * run store — so no path can be honouring it while another is not. It exists
10
+ * because a process that is not a delivery can invoke this CLI from a worktree
11
+ * that has a delivery run current: the repository's own test suite does,
12
+ * hundreds of times per `npm run check`, and every wrapped invocation appended
13
+ * a `command.completed` to whatever run the executor had open there. Such a
14
+ * process pins the override at a directory of its own and is thereby unable to
15
+ * reach any live store. An invocation with the override unset writes where it
16
+ * always did; an invocation under test with it unset is a defect in the test,
17
+ * not a behaviour of the product, so nothing here tries to detect a test
18
+ * runner.
19
+ *
20
+ * WHAT THE OVERRIDE DOES NOT COLLAPSE. Each repository keeps its own store
21
+ * under the named directory, at a subdirectory digested from its common
22
+ * directory, so the two rules the surface documents survive being overridden:
23
+ * two paths in different repositories are two stores, and two worktrees of one
24
+ * repository are one store. `repo.commonDir` on every event still names the
25
+ * repository, never the override — the event says where the run happened, not
26
+ * where its journal was filed.
27
+ *
6
28
  * WHY THE STORE IS RESOLVED HERE AND NOT WIRED. The record store is wired from
7
29
  * the config, because a delivery record belongs to a configured gate. A run
8
30
  * belongs to the *repository* — it outlives the worktree it ran in and it
@@ -25,7 +47,9 @@ import path from "node:path";
25
47
  import {
26
48
  createBlocker,
27
49
  createRunStore,
50
+ sha256Hex,
28
51
  evaluateRunJournal,
52
+ explainRunJournal,
29
53
  gitNamespaceClearedEnvironment,
30
54
  neutralizeForDisplay,
31
55
  resolveRunStoreLocation,
@@ -33,6 +57,7 @@ import {
33
57
  type Blocker,
34
58
  type RunEventInput,
35
59
  type RunEventKind,
60
+ type RunJournalRoundBinding,
36
61
  type RunJournalRow,
37
62
  type RunStore,
38
63
  } from "@agent-delivery-harness/kernel";
@@ -42,7 +67,9 @@ export const RUN_SURFACE_SOURCE = "delivery-harness.cli.run-surface";
42
67
 
43
68
  export interface RunSurface {
44
69
  readonly store: RunStore;
70
+ /** The repository's OWN git common directory, which is what an event names. */
45
71
  readonly commonDir: string;
72
+ /** The store's `runs` directory, under the override when one is named. */
46
73
  readonly runsDir: string;
47
74
  /** The invoking worktree's pointer key — the identity of "the current run" here. */
48
75
  readonly worktreeKey: string;
@@ -52,10 +79,39 @@ export type RunSurfaceResolution =
52
79
  | { readonly ok: true; readonly surface: RunSurface }
53
80
  | { readonly ok: false; readonly reason: string };
54
81
 
82
+ /** The environment variable that relocates the run store. See this file's header. */
83
+ export const RUN_STORE_OVERRIDE = "DELIVERY_HARNESS_RUN_STORE";
84
+
85
+ /**
86
+ * The directory the store is rooted at for a repository whose common directory
87
+ * is `commonDir`.
88
+ *
89
+ * Blank is unset, following this repository's other environment levers. A
90
+ * value that is set but not absolute FAILS CLOSED — the resolution is refused
91
+ * rather than falling back to the repository's own store, because a caller
92
+ * that mis-spells its override is exactly the caller that must not reach a
93
+ * live store by accident.
94
+ */
95
+ function storeRootFor(
96
+ commonDir: string,
97
+ override: string | undefined,
98
+ ): { readonly ok: true; readonly root: string } | { readonly ok: false; readonly reason: string } {
99
+ const named = override?.trim() ?? "";
100
+ if (named.length === 0) return { ok: true, root: commonDir };
101
+ if (!path.isAbsolute(named)) {
102
+ return { ok: false, reason: `${RUN_STORE_OVERRIDE} must name an absolute directory` };
103
+ }
104
+ return { ok: true, root: path.join(named, sha256Hex(commonDir)) };
105
+ }
106
+
55
107
  /**
56
108
  * Resolves the run store for the invoking worktree. Never throws: a config-free
57
109
  * command turns a failure into a typed blocker, and the boundary wrap turns it
58
110
  * into silence.
111
+ *
112
+ * The override is read here, on this one path, because this is the one path
113
+ * every CLI reach for a run store goes through: `emit`, `runs`, `runs serve`,
114
+ * the boundary wrap's automatic completion, and `verify`'s journal row.
59
115
  */
60
116
  export async function resolveRunSurface(cwd: string): Promise<RunSurfaceResolution> {
61
117
  const location = await resolveRunStoreLocation({
@@ -64,12 +120,15 @@ export async function resolveRunSurface(cwd: string): Promise<RunSurfaceResoluti
64
120
  env: gitNamespaceClearedEnvironment(),
65
121
  });
66
122
  if (!location.ok) return { ok: false, reason: location.reason };
123
+ const rooted = storeRootFor(location.commonDir, process.env[RUN_STORE_OVERRIDE]);
124
+ if (!rooted.ok) return { ok: false, reason: rooted.reason };
125
+ const store = createRunStore(rooted.root);
67
126
  return {
68
127
  ok: true,
69
128
  surface: {
70
- store: createRunStore(location.commonDir),
129
+ store,
71
130
  commonDir: location.commonDir,
72
- runsDir: location.runsDir,
131
+ runsDir: store.runsDir,
73
132
  worktreeKey: location.worktreeKey,
74
133
  },
75
134
  };
@@ -77,8 +136,11 @@ export async function resolveRunSurface(cwd: string): Promise<RunSurfaceResoluti
77
136
 
78
137
  /**
79
138
  * The worktree ROOT of a path, for the one thing the run surface needs it for:
80
- * the config-presence note names a root, and `runs serve` renders a root the
81
- * operator named rather than the one it was invoked in.
139
+ * the config-presence note names a root, and BOTH surfaces resolve it here.
140
+ * `runs serve` renders a root the operator named rather than the one it was
141
+ * invoked in; `runs show` renders the root of the worktree it was invoked in
142
+ * rather than the directory, which may be any depth below it. One question
143
+ * about one repository has to have one answer whichever surface asks it.
82
144
  *
83
145
  * `--show-toplevel` is a plumbing query that reads no index and runs no hook,
84
146
  * alias, or pager, and it runs here with the `GIT_` namespace cleared for the
@@ -122,6 +184,8 @@ export function buildRunEvent(input: {
122
184
  readonly kind: string;
123
185
  readonly role: "cli" | "executor";
124
186
  readonly payload: unknown;
187
+ readonly version?: "run-event/1" | "run-event/2";
188
+ readonly eventId?: string;
125
189
  }): RunEventInput {
126
190
  const payload = typeof input.payload === "object" && input.payload !== null ? (input.payload as Record<string, unknown>) : undefined;
127
191
  const mirrored: Record<string, unknown> = {};
@@ -133,7 +197,8 @@ export function buildRunEvent(input: {
133
197
  }
134
198
  }
135
199
  return {
136
- version: "run-event/1",
200
+ version: input.version ?? "run-event/1",
201
+ ...(input.eventId === undefined ? {} : { eventId: input.eventId }),
137
202
  runId: input.runId,
138
203
  at: runInstant(),
139
204
  repo: { commonDir: input.commonDir },
@@ -191,25 +256,133 @@ const ABSENT: RunJournalRow = { status: "absent", missing: [], attestation: "sel
191
256
  export async function resolveRunJournalRow(input: {
192
257
  readonly cwd: string;
193
258
  readonly treeSha: string;
259
+ readonly reviewedCandidateTreeShas?: readonly string[];
194
260
  readonly mandatedLensIds?: readonly string[];
195
261
  }): Promise<RunJournalRow> {
196
262
  const resolved = await resolveRunSurface(input.cwd);
197
263
  if (!resolved.ok) return ABSENT;
198
- const match = await resolved.surface.store.findByCandidateTreeSha(input.treeSha);
264
+ const acceptedTrees = [...new Set([input.treeSha, ...(input.reviewedCandidateTreeShas ?? [])])];
265
+ const matches = await Promise.all(acceptedTrees.map((treeSha) => resolved.surface.store.findByCandidateTreeSha(treeSha)));
266
+ const match = matches.find((entry) => entry !== undefined);
199
267
  if (match === undefined) return ABSENT;
200
268
  const read = await resolved.surface.store.read(match.runId);
201
269
  if (!read.ok) return ABSENT;
202
- const evaluation = evaluateRunJournal(read.events, input.treeSha, input.mandatedLensIds);
270
+ const evaluation = evaluateRunJournal(read.events, input.treeSha, input.mandatedLensIds, acceptedTrees.slice(1));
271
+ // The same arguments, so the explanations describe exactly these violations.
272
+ const diagnostics = explainRunJournal(read.events, input.treeSha, input.mandatedLensIds, acceptedTrees.slice(1));
273
+ const alsoMatching = [...new Set(matches.flatMap((entry) => entry === undefined ? [] : [entry.runId, ...entry.alsoMatching]))]
274
+ .filter((runId) => runId !== match.runId);
203
275
  return {
204
276
  runId: match.runId,
205
- ...(match.alsoMatching.length === 0 ? {} : { alsoMatching: match.alsoMatching }),
277
+ ...(alsoMatching.length === 0 ? {} : { alsoMatching }),
206
278
  status: evaluation.status,
207
279
  missing: evaluation.missing,
208
- ...(evaluation.violations.length === 0 ? {} : { violations: evaluation.violations }),
280
+ ...(evaluation.violations.length === 0 ? {} : { violations: evaluation.violations, explanations: diagnostics.explanations }),
281
+ ...(acceptedTrees.length === 1 ? {} : { recordTreeSha: input.treeSha, reviewedCandidateTreeShas: acceptedTrees.slice(1) }),
282
+ ...(diagnostics.roundBinding === undefined ? {} : { roundBinding: diagnostics.roundBinding }),
209
283
  attestation: "self",
210
284
  };
211
285
  }
212
286
 
287
+ /**
288
+ * The instants a delivery record stamps: the journal's own first and last.
289
+ *
290
+ * `endedAt` is the last instant the journal had REACHED when it was read, not
291
+ * the run's ending — `record` runs long before `run.ended`, and a record that
292
+ * waited for the run to end would never be written. So the pair is the span the
293
+ * delivery had spent by the time it recorded, which is the figure the record is
294
+ * for; `runs show` is where the run's own ending is read.
295
+ */
296
+ export interface RunSpanRow {
297
+ readonly runId: string;
298
+ readonly startedAt: string;
299
+ readonly endedAt: string;
300
+ }
301
+
302
+ /**
303
+ * The span of the journal that describes this candidate, or nothing.
304
+ *
305
+ * FOUND BY TREE SHA FIRST, for the reason `resolveRunJournalRow` is: a journal
306
+ * bound to this candidate is one that provably describes it, and by verify time
307
+ * the run has usually ended and cleared the worktree pointer. The pointer is a
308
+ * FALLBACK and only where the caller says so, because the two callers need
309
+ * different things from a miss. `record` is stamping: it runs mid-run, before
310
+ * anything need have journaled this exact tree, and the worktree's current run
311
+ * is the run it is recording from — a best-effort stamp is better than none.
312
+ * `verify` is refusing: a refusal must rest on a journal that binds the
313
+ * candidate the record binds, never on whichever run happens to be current in
314
+ * the checkout someone verified from, so it asks without the fallback and
315
+ * reports the miss as unchecked.
316
+ *
317
+ * MORE THAN ONE RUN CAN BIND ONE CANDIDATE, and the store says so: a delivery
318
+ * that recorded in run A and is verified from a later run B in the same
319
+ * worktree has two journals naming that tree, and `findByCandidateTreeSha`
320
+ * returns the most recently started one with the rest in `alsoMatching`.
321
+ * Refusing on whichever sorted first would block an honest record whose span
322
+ * run A reports exactly. So a caller that already knows the span it is checking
323
+ * passes `preferStartedAt`, and the matching run whose journal starts there is
324
+ * the one returned; only where NO matching run starts there does the
325
+ * top-ranked run stand, which is the case a refusal may rest on.
326
+ *
327
+ * Every failure is nothing found: no store, no match, an unreadable journal, an
328
+ * empty one. Nothing here decides what a miss means.
329
+ */
330
+ export async function resolveRunSpan(input: {
331
+ readonly cwd: string;
332
+ readonly treeSha: string;
333
+ readonly allowCurrentRun?: boolean;
334
+ readonly preferStartedAt?: string;
335
+ }): Promise<RunSpanRow | undefined> {
336
+ const resolved = await resolveRunSurface(input.cwd);
337
+ if (!resolved.ok) return undefined;
338
+ const matched = await resolved.surface.store.findByCandidateTreeSha(input.treeSha);
339
+ const runIds: string[] = matched === undefined ? [] : [matched.runId, ...matched.alsoMatching];
340
+ if (runIds.length === 0 && input.allowCurrentRun === true) {
341
+ const current = await resolved.surface.store.current(resolved.surface.worktreeKey);
342
+ if (current.ok && current.runId !== undefined) runIds.push(current.runId);
343
+ }
344
+ let fallback: RunSpanRow | undefined;
345
+ for (const runId of runIds) {
346
+ const read = await resolved.surface.store.read(runId);
347
+ if (!read.ok) continue;
348
+ const startedAt = read.events[0]?.at;
349
+ const endedAt = read.events[read.events.length - 1]?.at;
350
+ if (startedAt === undefined || endedAt === undefined) continue;
351
+ const row: RunSpanRow = { runId, startedAt, endedAt };
352
+ if (input.preferStartedAt === undefined || startedAt === input.preferStartedAt) return row;
353
+ fallback ??= row;
354
+ }
355
+ return fallback;
356
+ }
357
+
358
+ /**
359
+ * What the round binding means to someone deciding whether to act on the row.
360
+ *
361
+ * The `reviewed-tree` sentence is the one this ticket's readout exists for: it
362
+ * names WHERE the acceptance came from — the record's own verified evidence —
363
+ * and says in the same breath that the two trees differ, because the row prints
364
+ * both and an operator who reads "accepted" as "equal" would then wonder why.
365
+ */
366
+ const ROUND_BINDING_ROWS: Readonly<Record<RunJournalRoundBinding, string>> = {
367
+ "record-tree": "the governing closed round binds the record's own candidate tree",
368
+ "reviewed-tree":
369
+ "the governing closed round binds a reviewed candidate above, accepted from this record's verified review-neutral projection; the two trees differ and neither is claimed to equal the other",
370
+ unbound: "no closed round binds the record's candidate tree or any reviewed candidate it accepts",
371
+ };
372
+
373
+ /**
374
+ * The one true sentence about authority, printed wherever a violation is.
375
+ *
376
+ * An operator reading a list of violated constraints under a line that says
377
+ * `verified` is being asked a question the row cannot answer by itself: does
378
+ * this stop anything? It does not. The journal is appended to by anything the
379
+ * owner can execute, so no admission, gate, or record decision reads it — and
380
+ * the only thing that can be blocked by it is this command, behind the local
381
+ * opt-in the operator typed themselves.
382
+ */
383
+ export const RUN_JOURNAL_ADMISSION_ROW =
384
+ "none of the above blocks admission: the record's own evidence decides that, and no gate, admission, or record decision reads a journal. Only --require-run-journal blocks, and only this verify invocation.";
385
+
213
386
  /**
214
387
  * The row, rendered for a terminal. Every store-derived string goes through
215
388
  * `oneLine` for the same reason the viewer's rows do: a run id or a constraint
@@ -222,9 +395,23 @@ export function runJournalRows(row: RunJournalRow): readonly string[] {
222
395
  if (row.alsoMatching !== undefined && row.alsoMatching.length > 0) {
223
396
  rows.push(` also matching: ${row.alsoMatching.map((id) => oneLine(id, 128)).join(", ")}`);
224
397
  }
398
+ if (row.recordTreeSha !== undefined && row.reviewedCandidateTreeShas !== undefined) {
399
+ rows.push(` record candidate: ${oneLine(row.recordTreeSha, 64)}`);
400
+ rows.push(` reviewed candidate: ${row.reviewedCandidateTreeShas.map((tree) => oneLine(tree, 64)).join(", ")} (verified review-neutral projection)`);
401
+ }
402
+ if (row.roundBinding !== undefined) rows.push(` round binding: ${ROUND_BINDING_ROWS[row.roundBinding]}`);
225
403
  rows.push(` missing: ${row.missing.length === 0 ? "(none)" : row.missing.map((entry) => oneLine(entry, 64)).join(", ")}`);
226
404
  if (row.violations !== undefined && row.violations.length > 0) {
227
405
  rows.push(` violations: ${row.violations.map((entry) => oneLine(entry, 64)).join(", ")}`);
406
+ // One indented line per violation, in the order they were raised, each
407
+ // saying why it exists and — where it is only another warning restated —
408
+ // which warning that is. Product-authored text, bounded like every other
409
+ // store-derived row for the same reason.
410
+ for (const explanation of row.explanations ?? []) {
411
+ const consequence = explanation.consequenceOf === undefined ? "" : `; a consequence of ${oneLine(explanation.consequenceOf, 64)}, not a separate mistake`;
412
+ rows.push(` ${oneLine(explanation.violation, 64)}: ${oneLine(explanation.because, 400)}${consequence}`);
413
+ }
414
+ rows.push(` admission: ${RUN_JOURNAL_ADMISSION_ROW}`);
228
415
  }
229
416
  return rows;
230
417
  }
@@ -0,0 +1,77 @@
1
+ import { withRetainedRecord } from "./run-view-record.ts";
2
+ import { projectRunView } from "./run-view.ts";
3
+ import {
4
+ resolveRunSurface,
5
+ oneLine,
6
+ runSurfaceBlocker,
7
+ } from "./run-surface.ts";
8
+ import type { ConfigFreeCommandContext, CommandResult } from "./boundary.ts";
9
+ export async function runViewCommand(
10
+ context: ConfigFreeCommandContext,
11
+ args: readonly string[],
12
+ ): Promise<CommandResult> {
13
+ const runId = args[0];
14
+ let json = false;
15
+ let recordPath: string | undefined;
16
+ for (let i = 1; i < args.length; i++) {
17
+ if (args[i] === "--json" && !json) json = true;
18
+ else if (args[i] === "--record" && recordPath === undefined && args[i + 1])
19
+ recordPath = args[++i];
20
+ else
21
+ return {
22
+ kind: "usage",
23
+ message:
24
+ "Usage: runs view <run-id> [--json] [--record <repository-relative-path>]",
25
+ };
26
+ }
27
+ if (!runId)
28
+ return {
29
+ kind: "usage",
30
+ message:
31
+ "Usage: runs view <run-id> [--json] [--record <repository-relative-path>]",
32
+ };
33
+ const resolved = await resolveRunSurface(context.rootDir);
34
+ const read = resolved.ok
35
+ ? await resolved.surface.store.read(args[0]!)
36
+ : undefined;
37
+ if (!read?.ok)
38
+ return {
39
+ kind: "blocked",
40
+ blockers: [
41
+ runSurfaceBlocker({
42
+ code: "run_view_unavailable",
43
+ summary: "Run observations are unavailable.",
44
+ details: resolved.ok
45
+ ? "The selected run could not be read."
46
+ : oneLine(resolved.reason),
47
+ remediation: {
48
+ id: "inspect-run",
49
+ summary: "Check the selected repository and run identifier.",
50
+ },
51
+ }),
52
+ ],
53
+ };
54
+ const view = await withRetainedRecord(
55
+ projectRunView(read.events, { now: new Date().toISOString() }),
56
+ context.rootDir,
57
+ recordPath,
58
+ );
59
+ if (json) context.write(JSON.stringify(view));
60
+ else {
61
+ context.write(view.authority);
62
+ for (const section of view.sections) {
63
+ context.write(section.title);
64
+ if (!section.items.length) context.write(` ${section.empty}`);
65
+ for (const item of section.items) {
66
+ context.write(` ${oneLine(item.label)}`);
67
+ for (const field of item.fields)
68
+ context.write(` ${oneLine(field.label)}: ${oneLine(field.value, 8192)}`);
69
+ if (item.artifactId)
70
+ context.write(
71
+ ` Report: runs artifact ${oneLine(args[0]!)} ${oneLine(item.artifactId)} --json`,
72
+ );
73
+ }
74
+ }
75
+ }
76
+ return { kind: "ok" };
77
+ }
@@ -0,0 +1,138 @@
1
+ import { oneLine } from "./run-surface.ts";
2
+ import type { RunView, RunViewItem, RunViewSection } from "./run-view.ts";
3
+ import type {
4
+ RunArtifactResult,
5
+ RunArtifactMetadata,
6
+ } from "@agent-delivery-harness/kernel";
7
+ export const escapeViewHtml = (value: string) =>
8
+ value
9
+ .replace(/&/g, "&amp;")
10
+ .replace(/</g, "&lt;")
11
+ .replace(/>/g, "&gt;")
12
+ .replace(/"/g, "&quot;")
13
+ .replace(/'/g, "&#39;");
14
+ const text = (value: string) => escapeViewHtml(oneLine(value, 8192));
15
+ export const OPERATIONAL_STYLE = `
16
+ :root{color-scheme:light dark;--paper:#f5f5f7;--surface:#fff;--ink:#1d1d1f;--muted:#626267;--line:#d8d8de;--link:#0067c0;--wash:#eaf2fa}
17
+ *{box-sizing:border-box}body{font:400 1rem/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-optical-sizing:auto;letter-spacing:-.01em;color:var(--ink);background:var(--paper);max-width:76rem;margin:0 auto;padding:2.5rem 2rem 5rem;overflow-wrap:anywhere}
18
+ h1{font-size:2.25rem;line-height:1.12;letter-spacing:-.04em;margin:0 0 1.25rem;font-weight:700}h2{font-size:1.35rem;line-height:1.3;letter-spacing:-.025em;margin:2rem 0 .5rem}h3{font-size:1.3rem;line-height:1.3;letter-spacing:-.025em;color:var(--ink);margin:0 0 1rem}h4{font-size:1.05rem;line-height:1.35;margin:0 0 1rem;letter-spacing:-.015em}
19
+ p{margin:.6rem 0 1rem}.meta,.ended{color:var(--muted);font-size:.875rem}.labels{font-size:.875rem;color:var(--muted);background:var(--surface);border:1px solid var(--line);border-radius:.75rem;padding:1rem 1.25rem;margin:1rem 0}.live{color:var(--ink)}.open{color:var(--muted)}
20
+ a{color:var(--link);text-underline-offset:.2em;border-radius:.3rem}a:hover{text-decoration-thickness:2px}a:active,summary:active{background:var(--wash);color:var(--ink)}a:focus-visible,summary:focus-visible{outline:3px solid var(--link);outline-offset:3px}nav{display:flex;flex-wrap:wrap;gap:.4rem;margin:1.5rem 0 2.5rem}nav a{display:inline-flex;align-items:center;min-height:2.75rem;padding:.5rem .85rem;border:1px solid var(--line);border-radius:2rem;background:var(--surface);font-size:.875rem;text-decoration:none}nav a:hover{border-color:var(--link)}
21
+ section{scroll-margin-top:1.5rem;margin:2.5rem 0}section>p{color:var(--muted);font-size:.9375rem}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,23rem),1fr));gap:1rem}.card{background:var(--surface);border:1px solid var(--line);border-radius:1rem;padding:1.5rem;min-width:0}.card>a{display:inline-flex;align-items:center;min-height:2.75rem;margin-top:.5rem}dl{margin:.5rem 0}dt{font-size:.8125rem;color:var(--muted);font-weight:500;margin-top:.75rem}dd{margin:.15rem 0 .75rem;overflow-wrap:anywhere}details{margin:1rem 0;border-top:1px solid var(--line);padding-top:.5rem}summary{cursor:pointer;min-height:2.75rem;padding:.5rem 0;font-size:.875rem;color:var(--muted);border-radius:.3rem}
22
+ pre{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--surface);border:1px solid var(--line);border-radius:1rem;padding:1.5rem;font: .875rem/1.65 ui-monospace,SFMono-Regular,Menlo,monospace}.table-scroll{max-width:100%;overflow-x:auto;margin:1rem 0 2rem;border:1px solid var(--line);border-radius:.75rem}table{border-collapse:collapse;display:table;min-width:48rem;width:100%;font-size:.8125rem;margin:0}th,td{border:0;border-bottom:1px solid var(--line);padding:.75rem;text-align:left;vertical-align:top}th{background:var(--surface);font-weight:600}tr:last-child td{border-bottom:0}
23
+ .back{margin:.5rem 0}.run-identifiers,.view-provenance{margin:.25rem 0;border:0;padding:0}.section-navigation{margin:.25rem 0 1rem;border:0;padding:0}.section-navigation nav{margin:.5rem 0}body>details:first-of-type{margin:.5rem 0;border:0;padding:0}body>h1{margin-bottom:.5rem}section:first-of-type{margin-top:1.25rem}
24
+ @media(max-width:600px){body{padding:1.5rem 1rem 3rem;margin:0}h1{font-size:1.875rem}.cards{grid-template-columns:1fr}.card{padding:1.125rem}nav{gap:.375rem}section{margin:2rem 0}}
25
+ @media(prefers-color-scheme:dark){:root{--paper:#161618;--surface:#212124;--ink:#f5f5f7;--muted:#b1b1b8;--line:#45454b;--link:#8ac7ff;--wash:#263b50}}
26
+ @media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
27
+ @media(prefers-reduced-transparency:reduce){.card,nav a,.labels{background:var(--surface);backdrop-filter:none}}
28
+ @media(prefers-contrast:more){:root{--muted:var(--ink);--line:var(--ink)}a{text-decoration:underline}nav a{border-width:2px}}
29
+ body{max-width:68rem;padding-top:2rem}main{min-width:0}.page-header{margin:2rem 0}.page-header h1{margin:.4rem 0 .75rem}.eyebrow{font-size:.75rem;letter-spacing:.1em;text-transform:uppercase;font-weight:600;color:var(--muted);margin:0 0 .5rem}.toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:1rem;font-size:.8125rem;margin:1rem 0 2rem}.toolbar a{min-height:2.75rem;display:inline-flex;align-items:center}.toolbar .meta{margin-right:auto}.status{display:inline-flex;padding:.3rem .65rem;background:var(--wash);border-radius:2rem;font-size:.8125rem;font-weight:600}.run-list{display:grid;gap:.75rem}.run-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.75rem 2rem;background:var(--surface);border:1px solid var(--line);border-radius:1rem;padding:1.25rem 1.5rem}.run-row h3{font-size:1.125rem;margin:0}.run-row h3 a{display:inline-flex;align-items:center;min-height:2.75rem}.run-row p{margin:.25rem 0}.run-row .run-caption{grid-column:1/-1;font-size:.8125rem;color:var(--muted)}.now-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}.now-grid section{margin:0}.now-grid section h2{margin-top:.5rem}.now-grid .cards{grid-template-columns:1fr}.empty-note{margin:0;padding:1rem 0;font-size:.875rem}.supporting{margin-top:3rem}.supporting>summary{font-size:1rem;font-weight:600;color:var(--ink)}summary span{display:block;font-size:.8125rem;font-weight:400;color:var(--muted);margin:.25rem 0}.section-detail>section{margin-top:.5rem}.card{padding:1.25rem}.card h3{font-size:1rem;margin-bottom:.75rem}.card dl{display:grid;grid-template-columns:7rem minmax(0,1fr);gap:.35rem 1rem;margin:0}.card dt,.card dd{margin:0;font-size:.875rem}.card details{margin-bottom:0}.card details dl{display:block}.card details dt{margin-top:.75rem}.card details dd{margin-top:.2rem}.report-card{border-top:3px solid var(--line)}.report-link{font-weight:500;gap:.5rem}.notice{border-left:3px solid var(--line);padding:.75rem 1rem;background:var(--surface);border-radius:.25rem}.report-page{max-width:54rem}.report-document{background:var(--surface);border:1px solid var(--line);border-radius:1.25rem;padding:2.5rem}.report-document header h2{font-size:1.8rem;margin:.5rem 0 1rem}.report-document p{line-height:1.7;white-space:pre-wrap}.report-document pre{padding:0;border:0}.report-document section{margin:2rem 0}.report-document h3{font-size:1.2rem;line-height:1.4}.finding{border-top:1px solid var(--line);padding:1.75rem 0 .5rem}.finding h4{margin:1.5rem 0 .5rem}.count{font-size:.875rem;font-weight:400;color:var(--muted);margin-left:.5rem}.document-fields>dt{color:var(--ink);font-weight:600;font-size:.875rem;margin-top:1.5rem}.document-fields dd p{margin-top:.25rem}.document-list{padding-left:1.25rem}.document-list li{margin:.75rem 0}.report-tools{margin:1.5rem 0}.report-tools a{display:inline-flex;align-items:center;min-height:2.75rem}
30
+ @media(max-width:600px){.now-grid{grid-template-columns:1fr;gap:.5rem}.run-row{padding:1rem;gap:.5rem}.run-row .status{align-self:start}.card dl{grid-template-columns:5.5rem minmax(0,1fr)}.card dt{margin-top:0}.report-document{padding:1.25rem}.page-header{margin:1.5rem 0}.toolbar{gap:.5rem 1rem}}
31
+ `;
32
+ // Presentation consumes the shared projection; it never decides admission.
33
+ const field = (item: RunViewItem, label: string) => item.fields.find(f => f.label === label)?.value;
34
+ const friendlyLabels = new Map([
35
+ ["lens.outcome-correctness", "Outcome review"],
36
+ ["lens.adversarial-testing", "Testing review"],
37
+ ["review", "Review report"],
38
+ ["reduction", "Review summary"],
39
+ ["clarification", "Review clarification"],
40
+ ["partial-output", "Partial report"],
41
+ ]);
42
+ const friendly = (value: string) => friendlyLabels.get(value) ?? value;
43
+ const primaryLabels: Record<string, readonly string[]> = {
44
+ waiting: ["Owner", "Human action required", "Next action", "Freshness"],
45
+ work: ["Owner", "State", "Freshness", "Next step"],
46
+ reviews: ["Round", "Owner", "State", "Verdict", "Freshness", "Next step"],
47
+ reports: ["Availability", "Reason"],
48
+ finish: ["State", "Owner"],
49
+ findings: ["State", "Severity", "Deferred issue"],
50
+ };
51
+ function fieldsHtml(fields: RunViewItem["fields"]): string {
52
+ return fields.map(f => `<dt>${text(f.label)}</dt><dd>${text(f.value)}</dd>`).join("");
53
+ }
54
+ function card(item: RunViewItem, section: string, baseHref: string): string {
55
+ const labels = primaryLabels[section];
56
+ const primary = item.fields.filter(f => labels ? labels.includes(f.label) && f.value !== "Unreported" && !(section === "reports" && f.label === "Availability" && f.value === "referenced") : true);
57
+ const supporting = item.fields.filter(f => !primary.includes(f));
58
+ const title = section === "reports"
59
+ ? friendly(field(item, "Lens") === "Unreported" ? item.label : field(item, "Lens") ?? item.label)
60
+ : section === "work" ? label(field(item, "Phase") ?? "Delivery activity") : friendly(item.label);
61
+ return `<article data-key="${text(item.id)}" class="card ${section === "reports" ? "report-card" : ""}"><h3>${text(title)}</h3><dl>${fieldsHtml(primary)}</dl>${item.artifactId === undefined ? "" : `<a class="report-link" href="${text(baseHref)}/artifacts/${encodeURIComponent(item.artifactId)}">Read report <span aria-hidden="true">↗</span></a>`}${supporting.length ? `<details data-key="support-${text(item.id)}"><summary>Supporting details</summary><dl>${section === "work" ? `<dt>Activity</dt><dd>${text(item.label)}</dd>` : ""}${fieldsHtml(supporting)}</dl></details>` : ""}</article>`;
62
+ }
63
+ const sectionTitles: Record<string, string> = { waiting: "Needs attention", work: "In progress", reviews: "Reviews", reports: "Review reports", finish: "Delivery milestones" };
64
+ const emptyCopy: Record<string, string> = { waiting: "No waits reported.", reviews: "No current reviews reported.", reports: "Review reports will appear here when they are recorded.", finish: "Delivery milestones have not been reported." };
65
+ function sectionHtml(section: RunViewSection, baseHref: string): string {
66
+ return `<section id="${text(section.id)}"><h2>${text(sectionTitles[section.id] ?? section.title)}</h2>${section.items.length ? `<div class="cards">${section.items.map(i => card(i, section.id === "earlier-report-list" ? "reports" : section.id, baseHref)).join("")}</div>` : `<p class="empty-note">${text(emptyCopy[section.id] ?? section.empty)}</p>`}</section>`;
67
+ }
68
+ export function renderOperationalView(view: RunView, baseHref: string): string {
69
+ const sections = new Map(view.sections.map(s => [s.id, s]));
70
+ const render = (id: string) => sections.has(id) ? sectionHtml(sections.get(id)!, baseHref) : "";
71
+ const reviews = sections.get("reviews");
72
+ const work = sections.get("work");
73
+ const currentReviews = reviews?.items.filter(i => i.label !== "Declared review round" && field(i, "History") !== "Superseded attempt") ?? [];
74
+ const reviewIds = new Set(currentReviews.map(i => i.id));
75
+ const reviewCards = currentReviews.map(i => ({ ...i, fields: [...i.fields, ...(work?.items.find(w => w.id === i.id)?.fields.filter(f => ["Freshness", "Next step"].includes(f.label)) ?? [])] }));
76
+ if (work) sections.set("work", { ...work, items: work.items.filter(i => !reviewIds.has(i.id)), empty: work.items.some(i => reviewIds.has(i.id)) ? "Reviews are shown below." : work.empty });
77
+ const reports = sections.get("reports");
78
+ const currentReports = reports?.items.filter(i => field(i, "History") === "Latest observed attempt") ?? [];
79
+ const earlierReports = reports?.items.filter(i => !currentReports.includes(i)) ?? [];
80
+ const pastReviews = reviews?.items.filter(i => !currentReviews.includes(i)) ?? [];
81
+ return `${view.historical ? '<p class="notice">Historical archive · Observations retained at export.</p>' : ""}
82
+ <div class="now-grid">${render("waiting")}${render("work")}</div>
83
+ ${sections.get("findings")?.items.length ? render("findings") : ""}
84
+ ${reviews ? sectionHtml({ ...reviews, items: reviewCards }, baseHref) : ""}
85
+ ${reports ? sectionHtml({ ...reports, items: currentReports }, baseHref) : ""}
86
+ ${earlierReports.length && reports ? `<details id="earlier-reports"><summary>Earlier reports <span>${earlierReports.length} reports from earlier attempts or incomplete references</span></summary>${sectionHtml({ ...reports, id: "earlier-report-list", title: "Earlier reports", items: earlierReports }, baseHref)}</details>` : ""}${render("finish")}
87
+ <details id="supporting-evidence" class="supporting"><summary>Evidence, cost and history<span>Supporting facts and earlier observations</span></summary>
88
+ ${pastReviews.length && reviews ? sectionHtml({ ...reviews, id: "review-history", title: "Earlier reviews and round details", items: pastReviews }, baseHref) : ""}
89
+ ${["evidence", "cost", "activity-history", "finding-history"].map(id => `<details id="details-${id}" class="section-detail"><summary>${text(sections.get(id)?.title ?? id)} <span>${sections.get(id)?.items.length ?? 0} entries</span></summary>${render(id)}</details>`).join("")}
90
+ ${!sections.get("findings")?.items.length ? render("findings") : ""}</details>`;
91
+ }
92
+ const record = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
93
+ const label = (key: string) => key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").replace(/^./, c => c.toUpperCase());
94
+ // A bounded reading view for arbitrary report schemas. All content remains inert;
95
+ // the retained source and download are the authority for exact report bytes.
96
+ function documentValue(value: unknown, depth = 0): string {
97
+ if (depth > 4) return '<p class="meta">Further detail is available in the original report.</p>';
98
+ if (Array.isArray(value)) return value.length ? `<ul class="document-list">${value.slice(0, 40).map(v => `<li>${documentValue(v, depth + 1)}</li>`).join("")}</ul>${value.length > 40 ? '<p class="meta">Additional entries are available in the original report.</p>' : ""}` : '<p class="meta">None reported.</p>';
99
+ if (record(value)) return `<dl class="document-fields">${Object.entries(value).slice(0, 40).map(([k,v]) => `<dt>${text(label(k))}</dt><dd>${documentValue(v, depth + 1)}</dd>`).join("")}</dl>${Object.keys(value).length > 40 ? '<p class="meta">Additional fields are available in the original report.</p>' : ""}`;
100
+ return `<p>${text(value === null ? "Not reported" : String(value))}</p>`;
101
+ }
102
+ function structuredReport(source: string): string | undefined {
103
+ let parsed: unknown;
104
+ try { parsed = JSON.parse(source); } catch { return undefined; }
105
+ if (!record(parsed)) return undefined;
106
+ const result = parsed["outcome"] ?? parsed["verdict"] ?? parsed["decision"];
107
+ const findings = parsed["findings"];
108
+ const headline = result === "aligned" ? "Aligned" : (result === "dissent" || result === "changes-requested" || result === "CHANGES_REQUESTED") ? "Changes requested" : typeof result === "string" ? result : "Report contents";
109
+ const summary = parsed["summary"] ?? parsed["assessment"];
110
+ const rest = Object.fromEntries(Object.entries(parsed).filter(([k]) => !["outcome", "verdict", "decision", "findings", "summary", "assessment"].includes(k)));
111
+ return `<article class="report-document"><header><p class="eyebrow">Reported result</p><h2>${text(headline)}</h2>${summary === undefined ? "" : documentValue(summary)}</header>
112
+ ${Array.isArray(findings) ? `<section><h2>Findings <span class="count">${findings.length}</span></h2>${findings.length === 0 ? '<p>No findings reported in this document.</p>' : findings.slice(0, 40).map((finding, index) => {
113
+ if (!record(finding)) return documentValue(finding);
114
+ const title = typeof finding["title"] === "string" ? finding["title"] : `Finding ${index + 1}`;
115
+ const why = finding["why_it_matters"] ?? finding["description"];
116
+ const fix = finding["suggested_fix"];
117
+ const extra = Object.fromEntries(Object.entries(finding).filter(([k]) => !["title", "severity", "why_it_matters", "description", "suggested_fix"].includes(k)));
118
+ return `<article class="finding"><p class="eyebrow">${text(typeof finding["severity"] === "string" ? finding["severity"] : "Severity unreported")}</p><h3>${text(title)}</h3>${why === undefined ? "" : documentValue(why)}${fix === undefined ? "" : `<h4>Recommended change</h4>${documentValue(fix)}`}<details><summary>Evidence and context</summary>${documentValue(extra)}</details></article>`;
119
+ }).join("")}${findings.length > 40 ? '<p>Additional findings are available in the original report.</p>' : ""}</section>` : findings === undefined ? "" : `<section><h2>Findings</h2>${documentValue(findings)}</section>`}
120
+ <details><summary>Review context and supporting evidence</summary>${documentValue(rest)}</details></article>`;
121
+ }
122
+ export function renderArtifactDetail(input: {
123
+ runId: string;
124
+ artifactId: string;
125
+ backHref: string;
126
+ result: RunArtifactResult;
127
+ historical: boolean;
128
+ metadata?: RunArtifactMetadata;
129
+ }): string {
130
+ const { result } = input;
131
+ const m = result.ok ? result.metadata : input.metadata;
132
+ const source = result.ok ? Buffer.from(result.base64, "base64").toString("utf8") : "";
133
+ const document = result.ok ? structuredReport(source) : undefined;
134
+ const title = friendly(m?.lensId ?? "Review report");
135
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${text(title)} · Delivery runs</title><style>${OPERATIONAL_STYLE}</style></head><body class="report-page"><main><a class="back" href="${text(input.backHref)}#reports">Return to run</a><header class="page-header"><p class="eyebrow">${input.historical ? "Historical archive" : "Retained report"}${m?.round === undefined ? "" : ` · Round ${m.round}`}</p><h1>${text(title)}</h1><p class="meta">Reported review output; not approval evidence.</p></header>
136
+ ${result.ok ? `${document ?? `<article class="report-document"><h2>Report contents</h2><pre>${escapeViewHtml(source)}</pre></article>`}<div class="report-tools"><a href="${text(input.backHref)}/artifacts/${encodeURIComponent(input.artifactId)}/download">Download original report</a></div>${document ? `<details><summary>Original report source</summary><pre>${escapeViewHtml(source)}</pre></details>` : ""}` : `<section class="notice" role="status"><h2>Report unavailable</h2><p>${text(result.reason)}</p><p class="meta">${text(result.code)}</p></section>`}
137
+ <details><summary>Report details and provenance</summary><dl><dt>Run</dt><dd>${text(input.runId)}</dd><dt>Artifact</dt><dd>${text(input.artifactId)}</dd>${m ? `<dt>Lens</dt><dd>${text(m.lensId ?? "Unreported")}</dd><dt>Attempt</dt><dd>${text(m.attemptId)}</dd><dt>Candidate</dt><dd>${text(m.candidateTreeSha)}</dd><dt>Digest</dt><dd>${text(m.digest)}</dd><dt>Size</dt><dd>${m.sizeBytes} bytes</dd>` : ""}</dl></details></main></body></html>`;
138
+ }
@@ -0,0 +1,119 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { readArchiveFile } from "./run-archive-commands.ts";
4
+ /** Explicit retained-record reads only: no executable configuration or verification. */
5
+ import {
6
+ isSafeRelativePath,
7
+ isInsideResolved,
8
+ parseDeliveryRecord,
9
+ digestCanonical,
10
+ } from "@agent-delivery-harness/kernel";
11
+ import type { RunView, RunViewItem } from "./run-view.ts";
12
+ export async function withRetainedRecord(
13
+ view: RunView,
14
+ root: string,
15
+ relativePath?: string,
16
+ ): Promise<RunView> {
17
+ if (relativePath === undefined || view.historical) return view;
18
+ const fields: { label: string; value: string }[] = [];
19
+ const add = (label: string, value: string) => fields.push({ label, value });
20
+ add("Source", relativePath);
21
+ add(
22
+ "Applicability",
23
+ "Unknown — no current identity comparison or fresh verification is performed",
24
+ );
25
+ add(
26
+ "Original verification time",
27
+ "Unavailable — delivery records do not store a verification timestamp",
28
+ );
29
+ try {
30
+ let status = "path_refused";
31
+ let contents: string | null = null;
32
+ if (isSafeRelativePath(relativePath)) {
33
+ const resolvedRoot = await realpath(root);
34
+ const resolvedFile = await realpath(
35
+ path.join(resolvedRoot, relativePath),
36
+ );
37
+ if (!isInsideResolved(resolvedRoot, resolvedFile))
38
+ status = "outside_run_root";
39
+ else {
40
+ contents = await readArchiveFile(resolvedFile);
41
+ status = "readable";
42
+ }
43
+ }
44
+ const read = { status, contents };
45
+ if (read.status !== "readable" || read.contents === null) {
46
+ add("Status", `Unavailable — ${read.status}`);
47
+ } else {
48
+ const parsed = parseDeliveryRecord(read.contents);
49
+ if (!parsed.ok)
50
+ add("Status", "Corrupt — retained record grammar is invalid");
51
+ else {
52
+ const { integrityDigest, ...unsigned } = parsed.record;
53
+ if (parsed.record.version !== "delivery-record/2")
54
+ add(
55
+ "Status",
56
+ "Legacy record — integrity unavailable; no accepted evidence inferred",
57
+ );
58
+ else if (integrityDigest !== digestCanonical(unsigned))
59
+ add("Status", "Corrupt — retained record digest does not match");
60
+ else {
61
+ add(
62
+ "Status",
63
+ "Recorded for the candidate below — retained self-attested record, not current approval",
64
+ );
65
+ add("Candidate", parsed.record.candidateBinding.treeSha);
66
+ add(
67
+ "Deliverable digest",
68
+ parsed.record.candidateBinding.deliverableDigest,
69
+ );
70
+ for (const claim of parsed.record.claims) {
71
+ add(`Recorded claim · ${claim.obligationId}`, claim.outcome);
72
+ for (const evidence of [
73
+ claim.evidence,
74
+ ...(claim.supportingEvidence ?? []),
75
+ ]) {
76
+ if (evidence?.resolution.kind !== "evidence") continue;
77
+ const manifest = evidence.resolution.portable?.manifest;
78
+ if (typeof manifest !== "object" || manifest === null) continue;
79
+ if (
80
+ digestCanonical(manifest) !== evidence.resolution.manifestDigest
81
+ ) {
82
+ add(
83
+ "Retained manifest",
84
+ "Corrupt — digest does not match; timestamp unavailable",
85
+ );
86
+ continue;
87
+ }
88
+ const at = (manifest as Record<string, unknown>)["recordedAt"];
89
+ add(
90
+ `Provider-reported recording time · ${evidence.resolution.providerId}`,
91
+ typeof at === "string" ? at : "Unavailable",
92
+ );
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ } catch (error) {
99
+ add(
100
+ "Status",
101
+ (error as NodeJS.ErrnoException).code === "ENOENT"
102
+ ? "Unavailable — missing"
103
+ : "Unavailable — bounded retained record read refused",
104
+ );
105
+ }
106
+ const retained: RunViewItem = {
107
+ id: "retained-record",
108
+ label: "Retained delivery record",
109
+ fields,
110
+ };
111
+ return {
112
+ ...view,
113
+ sections: view.sections.map((section) =>
114
+ section.id === "evidence"
115
+ ? { ...section, items: [retained, ...section.items] }
116
+ : section,
117
+ ),
118
+ };
119
+ }