Compare commits

...
Author SHA1 Message Date
review50-r3andClaude Opus 5.5 e086fd343b fix(execution): on the default flow the ladder shows each slice's recorded status and says when nothing records a build
A team on the default flow records a slice's status: word, not plan lock, candidate:<sha> rows, review artifacts or a
worktree_path= row, so a finished mission's ladder read all false or INDETERMINATE.

- q4_ladder[].recorded_status: the status: word and its bucket (as GET /api/slices buckets it), labelled as the team's
  record; it never sets a rung or next-up. No status: reads word null, not draft.
- q4_records: how many slices have a candidate:<sha> row or a merged:<pr> report, the repository context, and a basis
  that says "not recorded, which is not the same as not done" when nothing records a build.
- Plan lock still reads false and now says it's a heavier-flow step.
- TUI: done-bucket words (shipped-to-<mission>, closed-<reason>, promoted) read declared done, not planned; the strip's
  next says none; the evidence page shows the records line. An older daemon keeps the old rule.
- sliceStatusBucket is the slice indexer's bucket rule, exported unchanged so both surfaces agree.

Refs #2761. Reported by @ziadeh.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 01:28:17 +00:00
review50-r3andClaude Opus 5.5 e15cebdf67 test(execution): cells for the ladder on the default flow (red without the fix)
A mission finished on the default flow: slices with status: done, shipped-to-<mission>, Closed and none, and rows that
name the mission but carry no candidate:<sha> or worktree_path=. The daemon cells expect each slice's recorded status
beside its ladder, a mission-level records line, unchanged rungs and next-up, and plan lock's reason. The TUI cells
expect done-bucket words to read declared done, the strip's next to say none, the evidence page's records line, and an
older daemon to keep the old rule.

Refs #2761. Reported by @ziadeh.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 01:26:46 +00:00
6 changed files with 253 additions and 13 deletions

No files matched your search

+9
View File
@@ -161,6 +161,15 @@ or `pending`, `draft`. A word that matches none of these counts as `active`, so
slice marked `complete` counts as `active`. A slice with no `status:` counts as
`draft`.
`rig view show execution` shows the same word and bucket beside each slice's
ladder as `recorded_status` (`word: null` when there's no `status:`). It's the
team's record, not verified, so it never turns a rung true or makes a slice next-up.
The ladder's rungs read Part B records: plan lock, `candidate:<sha>` rows, review
artifacts and an EC-3 `worktree_path=` row. On this flow they read `INDETERMINATE`
(plan lock reads `false`), and `q4_records` says once for the mission that no
build is recorded. Built means a Git commit, so work that isn't a commit never
reads built.
When the owning scope selects `proofPolicy.judges`, use `rig proof judge` to
record an attributed accept, reject or withdraw decision on a contract item,
and `rig proof show` to read derived readiness up through slice, mission and
+26 -3
View File
@@ -18,6 +18,8 @@ import { isTypedGateBlocker } from "./queue-repository.js";
// "INDETERMINATE" (with a named basis), never idle/dead/done/false.
// - THE LADDER IS THE SCHEMA: done-ness is five named rungs
// (locked/built/reviewed/folded/adopted); a single "done" boolean does not exist.
// What a team recorded about a slice (its `status:` word, a `merged:<pr>` report)
// is shown beside the rungs as labelled testimony and never sets one.
// - PROJECTION, NOT AUTHORITY: every cell carries the row/artifact/command that
// makes it one command from source.
//
@@ -38,7 +40,7 @@ import { promisify } from "node:util";
import { parse as parseYaml } from "yaml";
import type Database from "better-sqlite3";
import { shellQuote } from "../adapters/shell-quote.js";
import { parseFrontmatter } from "./slices/slice-indexer.js";
import { parseFrontmatter, sliceStatusBucket } from "./slices/slice-indexer.js";
import { lastMeaningfulTransition } from "./queue-waiting.js";
import { derivePickup } from "./queue-pickup.js";
@@ -986,7 +988,13 @@ export async function buildExecutionView(deps: ExecutionViewDeps, opts?: { missi
const fm = facts.frontmatter;
const locked: Rung = typeof fm["approved-spec-at"] === "string"
? { value: true, basis: `frontmatter approved-spec-at=${fm["approved-spec-at"]}` }
: { value: false, basis: "no approved-spec-at in frontmatter" };
: { value: false, basis: "no approved-spec-at in frontmatter; plan lock is a Part B step (rig scope slice approve --scope spec), so the default flow reads false here whatever the recorded status" };
// The team's own word for the slice, as `GET /api/slices` buckets it. Testimony: it never sets a rung or next-up.
const rawStatus = fm["status"];
const statusWord = typeof rawStatus === "string" && rawStatus.trim() ? rawStatus.trim() : null;
const recorded_status = statusWord
? { word: statusWord, bucket: sliceStatusBucket(statusWord), basis: "slice frontmatter status:, recorded by the team, not verified; it never sets a rung" }
: { word: null, bucket: null, basis: "no status: in slice frontmatter" };
// built: latest candidate:<sha> tag on a row bound to this slice.
const candRows = rows
.filter((r) => sliceOfRow(r) === facts.id || sliceOfRow(r) === facts.dir)
@@ -1110,7 +1118,7 @@ export async function buildExecutionView(deps: ExecutionViewDeps, opts?: { missi
: buildInfo.commit
? await gitAncestor(exec, repoCtx, builtResolved ?? candidateSha, buildInfo.commit)
: { value: INDETERMINATE, basis: "daemon build stamp absent (dev run) — adopted rung underivable" };
return { slice_id: facts.id, dir: facts.dir, locked, built, reviewed, folded, adopted };
return { slice_id: facts.id, dir: facts.dir, recorded_status, locked, built, reviewed, folded, adopted };
};
const ladderCache = new Map<string, Record<string, unknown>>();
@@ -1124,6 +1132,20 @@ export async function buildExecutionView(deps: ExecutionViewDeps, opts?: { missi
// so mutable refs (HEAD/main) are observed again on the next request.
const q4: Record<string, unknown>[] = [];
for (const s of slices) q4.push(await ladderFor(s));
// #2761: say once what the mission records for the ladder, so missing records never read as missing work.
const withCandidate = q4.filter((l) => (l["built"] as { candidate_sha?: unknown }).candidate_sha !== INDETERMINATE).length;
const withMergeReport = q4.filter((l) => !!(l["folded"] as { merge_report?: unknown }).merge_report).length;
const q4Records = {
slices: q4.length,
with_candidate: withCandidate,
with_merge_report: withMergeReport,
repo_context: repoCtx ?? INDETERMINATE,
basis: q4.length === 0
? "no slices in this mission"
: withCandidate === 0
? "no slice in this mission has a candidate:<sha> row, so built, reviewed and merged read INDETERMINATE: not recorded, which is not the same as not done. The default flow (sdlc-conventions.md Part A) doesn't record them; each slice's recorded_status is the team's own word"
: `${withCandidate} of ${q4.length} slices have a candidate:<sha> row; ${repoCtx ? `merged is tested in repo context ${repoCtx}` : "no row carries worktree_path= (EC-3), so merged can't be tested"}`,
};
// ---- Q2 sequencing ----
const q2 = [];
@@ -1382,6 +1404,7 @@ export async function buildExecutionView(deps: ExecutionViewDeps, opts?: { missi
q2_sequencing: q2,
q3_care: q3,
q4_ladder: q4,
q4_records: q4Records,
q5_park: q5,
q6_parallelism: q6,
};
@@ -266,6 +266,18 @@ const STATUS_TO_BUCKET: Record<string, SliceStatus> = {
"parked-with-evidence": "blocked",
};
/** The bucket a slice's frontmatter `status:` word falls in, as `GET /api/slices` reports it (sdlc-conventions.md A2). */
export function sliceStatusBucket(raw: string | undefined): SliceStatus {
if (!raw) return "draft";
const normalized = raw.toLowerCase().trim();
if (STATUS_TO_BUCKET[normalized]) return STATUS_TO_BUCKET[normalized];
// Heuristic fallbacks.
if (normalized.includes("done") || normalized.includes("ship") || normalized.includes("close")) return "done";
if (normalized.includes("block") || normalized.includes("park")) return "blocked";
if (normalized.includes("draft") || normalized.includes("pending")) return "draft";
return "active";
}
export class SliceIndexer {
readonly projectId?: string;
readonly missionId?: string;
@@ -765,14 +777,7 @@ export class SliceIndexer {
}
private mapStatus(raw: string | undefined): SliceStatus {
if (!raw) return "draft";
const normalized = raw.toLowerCase().trim();
if (STATUS_TO_BUCKET[normalized]) return STATUS_TO_BUCKET[normalized];
// Heuristic fallbacks.
if (normalized.includes("done") || normalized.includes("ship") || normalized.includes("close")) return "done";
if (normalized.includes("block") || normalized.includes("park")) return "blocked";
if (normalized.includes("draft") || normalized.includes("pending")) return "draft";
return "active";
return sliceStatusBucket(raw);
}
private matchQitems(sliceName: string, served: Set<string> | null): string[] {
@@ -0,0 +1,121 @@
// #2761: on the default flow (sdlc-conventions Part A) a team records a slice's `status:`, not plan lock, `candidate:`
// rows, review artifacts or an EC-3 worktree row. The execution view must say what it couldn't read instead of reading
// finished work as not done: each slice shows its recorded status beside the ladder (the team's word, never a rung), and
// the mission says once that no build records exist. The rungs and next-up stay exactly as their own records give them.
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import type Database from "better-sqlite3";
import { createDb } from "../src/db/connection.js";
import { migrate } from "../src/db/migrate.js";
import { coreSchema } from "../src/db/migrations/001_core_schema.js";
import { bindingsSessionsSchema } from "../src/db/migrations/002_bindings_sessions.js";
import { eventsSchema } from "../src/db/migrations/003_events.js";
import { queueItemSummarySchema } from "../src/db/migrations/044_queue_item_summary.js";
import { queueItemsSchema } from "../src/db/migrations/024_queue_items.js";
import { queueTransitionsSchema } from "../src/db/migrations/025_queue_transitions.js";
import { queueTransitionWakesSchema } from "../src/db/migrations/073_queue_transition_wakes.js";
import { viewsCustomSchema } from "../src/db/migrations/030_views_custom.js";
import { EventBus } from "../src/domain/event-bus.js";
import { ViewProjector } from "../src/domain/view-projector.js";
const MISSION = "simple-flow";
type Doc = Record<string, unknown>;
type Entry = Record<string, Record<string, unknown>> & { slice_id: string };
describe("#2761 execution view on the default flow", () => {
let db: Database.Database;
let projector: ViewProjector;
let tmp: string;
let missionsRoot: string;
const slice = (dir: string, frontmatter: string[]) => {
const d = path.join(missionsRoot, MISSION, "slices", dir);
fs.mkdirSync(d, { recursive: true });
fs.writeFileSync(path.join(d, "SPEC.md"), `---\n${frontmatter.join("\n")}\n---\n\n## Intent\n${dir}\n`);
};
const row = (id: string, tags: string[], body = "Work.") =>
db.prepare(`INSERT INTO queue_items (qitem_id, ts_created, ts_updated, source_session, destination_session, state, priority, tier, tags, body)
VALUES (?, '2026-11-01T00:00:00.000Z', '2026-11-01T00:00:00.000Z', 'lead@t', 'builder@t', 'done', 'normal', 'light', ?, ?)`)
.run(id, JSON.stringify(tags), body);
const show = async (): Promise<Doc> => (await projector.show("execution", { mission: MISSION })).rows[0] as Doc;
const ladder = (doc: Doc) => doc.q4_ladder as Entry[];
const entry = (doc: Doc, id: string) => ladder(doc).find((e) => e.slice_id === id)!;
beforeEach(() => {
tmp = fs.mkdtempSync(path.join(os.tmpdir(), "exec-default-flow-"));
missionsRoot = path.join(tmp, "missions");
fs.mkdirSync(path.join(missionsRoot, MISSION), { recursive: true });
fs.writeFileSync(path.join(missionsRoot, MISSION, "SPEC.md"), `---\nid: SF\nmission: ${MISSION}\n---\n# Simple flow\n`);
// Finished on the default flow: the team's own words, as `rig scope slice ship`/`close` and hand edits write them.
slice("01-done", ["id: SF.1", "status: done", "depends_on: []"]);
slice("02-shipped", ["id: SF.2", "status: shipped-to-release-1", 'depends_on: ["SF.1"]']);
slice("03-closed", ["id: SF.3", "status: Closed", "depends_on: []"]);
// Scaffolded after the work, with no status at all (the reporter's 9-slice mission).
slice("04-unrecorded", ["id: SF.4", "depends_on: []"]);
db = createDb();
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, queueItemsSchema, queueItemSummarySchema,
queueTransitionsSchema, queueTransitionWakesSchema, viewsCustomSchema]);
projector = new ViewProjector(db, new EventBus(db));
projector.setExecutionDeps({
db,
slicesRoot: () => missionsRoot,
rigsRoot: () => path.join(tmp, "rigs"),
buildInfo: { semver: null, commit: null, dirty: null, builtAt: null },
now: () => new Date("2026-11-10T00:00:00.000Z"),
seatActivity: { getSeatStateBySession: () => null },
} as never);
// The team's rows name the mission, as Part A asks; none carries candidate: or worktree_path=.
row("qitem-sf-1", [`mission:${MISSION}`, "slice:01-done"]);
row("qitem-sf-2", [`mission:${MISSION}`, "slice:build-two"]);
});
afterEach(() => {
fs.rmSync(tmp, { recursive: true, force: true });
db.close();
});
it("shows each slice's recorded status and its bucket beside the ladder, as testimony", async () => {
const doc = await show();
expect(entry(doc, "SF.1").recorded_status).toMatchObject({ word: "done", bucket: "done" });
expect(entry(doc, "SF.2").recorded_status).toMatchObject({ word: "shipped-to-release-1", bucket: "done" });
expect(entry(doc, "SF.3").recorded_status).toMatchObject({ word: "Closed", bucket: "done" });
expect(entry(doc, "SF.1").recorded_status!.basis).toMatch(/recorded by the team, not verified; it never sets a rung/);
// No status recorded is said as such, not read as draft or as not done.
expect(entry(doc, "SF.4").recorded_status).toEqual({ word: null, bucket: null, basis: "no status: in slice frontmatter" });
});
it("says once for the mission that nothing records a build, so built and merged read not recorded", async () => {
const records = (await show()).q4_records as Record<string, unknown>;
expect(records).toMatchObject({ slices: 4, with_candidate: 0, with_merge_report: 0, repo_context: "INDETERMINATE" });
expect(records.basis).toMatch(/^no slice in this mission has a candidate:<sha> row/);
expect(records.basis).toMatch(/not recorded, which is not the same as not done/);
});
it("keeps every rung and next-up as their own records give them: a recorded status never turns one", async () => {
const doc = await show();
for (const e of ladder(doc)) {
expect(e.built!.candidate_sha).toBe("INDETERMINATE");
expect(e.reviewed!.value).toBe("INDETERMINATE");
expect(e.folded!.value).toBe("INDETERMINATE");
}
// Plan lock is a Part B step: false stays false, and the basis says why it reads false on this flow.
expect(entry(doc, "SF.1").locked).toEqual({
value: false,
basis: "no approved-spec-at in frontmatter; plan lock is a Part B step (rig scope slice approve --scope spec), so the default flow reads false here whatever the recorded status",
});
// Next-up reads the folded rung, not the status: a done-recorded slice stays INDETERMINATE, never dispatchable.
const q2 = doc.q2_sequencing as Array<Record<string, unknown>>;
expect(q2.find((s) => s.slice_id === "SF.1")).toMatchObject({ next_up: "INDETERMINATE" });
expect(q2.find((s) => s.slice_id === "SF.4")).toMatchObject({ next_up: "INDETERMINATE" });
});
it("counts recorded builds and merge reports when a team does record them", async () => {
row("qitem-sf-built", [`mission:${MISSION}`, "slice:01-done", "candidate:0123456789abcdef0123456789abcdef01234567"]);
row("qitem-sf-merged", [`mission:${MISSION}`, "slice:03-closed", "merged:41"]);
const records = (await show()).q4_records as Record<string, unknown>;
expect(records).toMatchObject({ slices: 4, with_candidate: 1, with_merge_report: 1, repo_context: "INDETERMINATE" });
expect(records.basis).toMatch(/^1 of 4 slices have a candidate:<sha> row; no row carries worktree_path= \(EC-3\), so merged can't be tested/);
});
});
+13 -2
View File
@@ -29,6 +29,8 @@ export interface ExecutionViewSnap {
q2_sequencing: Array<Record<string, unknown>>;
q3_care?: Array<Record<string, unknown>>;
q4_ladder: Array<Record<string, unknown>>;
/** #2761: what the mission records for the ladder (absent on older daemons). */
q4_records?: Record<string, unknown>;
q5_park: Array<Record<string, unknown>>;
q6_parallelism?: Record<string, unknown>;
/** S06: existing workflow engine facts joined to the selected mission. */
@@ -220,6 +222,13 @@ function declaredText(slice: SliceFacts): string {
return slice.scope?.status?.trim().toLowerCase() || "no declared status";
}
/** A declared word in the done bucket (done, shipped, promoted, closed…), as the daemon buckets it beside the ladder;
* an older daemon serves no bucket, and only the word "done" counts. */
function declaredDone(ladder: Record<string, unknown>, word: string): boolean {
const bucket = record(ladder["recorded_status"])["bucket"];
return typeof bucket === "string" ? bucket === "done" : word === "done";
}
function seatShort(seat: unknown): string {
const full = str(seat, "");
return full.includes("@") ? full.slice(0, full.indexOf("@")) : full;
@@ -267,7 +276,7 @@ function stateWord(slice: SliceFacts): string {
if (slice.readiness?.configured) return "outcomes pending";
if (stageText(slice) === "retired" || declaredText(slice) === "retired") return "retired";
if (declaredText(slice) === "deferred" || declaredText(slice) === "closed-deferred") return "deferred";
return declaredText(slice) === "done" ? "declared done" : "planned";
return declaredDone(slice.ladder, declaredText(slice)) ? "declared done" : "planned";
}
function proofText(scope: SliceScopeSnap | null): string | null {
@@ -426,6 +435,7 @@ function collectIndeterminate(execution: ExecutionViewSnap, slices: SliceFacts[]
function evidenceDetail(execution: ExecutionViewSnap, slices: SliceFacts[], width: number, timeZone = DEFAULT_TIME_ZONE): ContentLine[] {
const attributed = slices.filter(slice => slice.readiness?.configured);
const gitBasis = str(record(execution.sources?.["git"])["basis"], "(no git source cell)");
const recordsBasis = record(execution.q4_records)["basis"];
const lines: ContentLine[] = [
{ text: `${execution.mission} · ${attributed.length ? "proof provenance" : "evidence gap"} · derived ${displayTime(execution.derived_at, timeZone) || "?"}` },
{ text: "" },
@@ -441,6 +451,7 @@ function evidenceDetail(execution: ExecutionViewSnap, slices: SliceFacts[], widt
}
lines.push({ text: "" }, sectionRule("code lineage · separate from item judgments", width),
{ text: ` git: ${gitBasis}` },
...(typeof recordsBasis === "string" ? wrapDetailLines([{ text: ` records: ${recordsBasis}` }], width) : []),
...wrapDetailLines([{ text: " Build, review and merge facts remain on each slice's code evidence. Daemon adoption applies only to the unscoped daemon-source view. Artifact acceptance supplies none of these code facts." }], width));
for (const item of collectIndeterminate(execution, slices)) {
lines.push({ text: "" }, sectionRule(`${item.where} unconfirmed for ${item.members.length} slice${item.members.length === 1 ? "" : "s"}`, width));
@@ -845,7 +856,7 @@ export function executionSliceStripLines(
const evidence = `${evidenceText(slice.cells, slice.rank)}${unconfirmed.length ? ` · ${unconfirmed.join(" / ")} unconfirmed (${slice.cells[RUNGS.find((rung) => slice.cells[rung].state === "undetermined")!].basis})` : ""}`;
const liveWord = slice.lane ? (waitingOn(slice.lane) ? "waiting" : str(activity["activity"], "claimed")) : "no claimed lane";
const declaredWord = declared?.trim().toLowerCase() || "no declared status";
const next = (declaredWord === "done" || declaredWord === "retired" || declaredWord === "deferred" || declaredWord === "closed-deferred") && !slice.lane
const next = (declaredWord === "retired" || declaredWord === "deferred" || declaredWord === "closed-deferred" || declaredDone(slice.ladder, declaredWord)) && !slice.lane
? `none — declared ${declaredWord}`
: nextText(slice) ?? (slice.lane ? (waitingOn(slice.lane) ? `waiting on ${waitingOn(slice.lane)}, see the waiting row above` : "in progress on the lane above") : "nothing the projection can sequence");
return [
@@ -0,0 +1,71 @@
// #2761: on the default flow a team records each slice's `status:` word (`rig scope slice ship` writes
// `shipped-to-<mission>`, `close` writes `closed-<reason>`) and no build records. The mission view must read every word
// in the done bucket as declared done, not "planned", and say once that the ladder's build records weren't kept.
import { describe, expect, it } from "vitest";
import { executionContentLines, executionSliceStripLines } from "../src/execution/execution-model.js";
import { demoSnapshot } from "../src/demo-data.js";
import type { FleetSnapshot } from "../src/types.js";
const WORDS = ["shipped-to-release-1", "closed", "promoted"];
const RECORDS = "no slice in this mission has a candidate:<sha> row, so built, reviewed and merged read INDETERMINATE: not recorded, which is not the same as not done";
const UNRECORDED = "no candidate:* tag on any row bound to this slice (unbuilt and unrecorded are indistinguishable here)";
/** A finished default-flow mission. `served` is a daemon that serves recorded_status and q4_records; without it, an older one. */
function finished(served: boolean) {
const ids = WORDS.map((_, i) => `SF.${i + 1}`);
const dir = (i: number) => `0${i + 1}-slice`;
const execution = {
view: "execution",
mission: "simple-flow",
derived_at: "2026-11-10T00:00:00.000Z",
sources: { git: { basis: "no reachable repo context" }, build_info: { commit: null } },
q1_lanes: [],
q2_sequencing: ids.map((id, i) => ({ slice_id: id, dir: dir(i), depends_on: [], blocked_on_rows: [], next_up: "INDETERMINATE",
next_up_basis: "own completion rung INDETERMINATE (no candidate sha to test)", next_up_rank: null })),
q3_care: ids.map((id) => ({ slice_id: id, build_wave: "w1" })),
q4_ladder: ids.map((id, i) => ({
slice_id: id,
dir: dir(i),
...(served ? { recorded_status: { word: WORDS[i], bucket: "done", basis: "slice frontmatter status:, recorded by the team, not verified; it never sets a rung" } } : {}),
locked: { value: false, basis: "no approved-spec-at in frontmatter" },
built: { candidate_sha: "INDETERMINATE", basis: UNRECORDED },
reviewed: { value: "INDETERMINATE", basis: "no built candidate token to scope review legs to", legs: [] },
folded: { value: "INDETERMINATE", basis: "no candidate sha to test" },
adopted: { value: "INDETERMINATE", basis: "no candidate sha to test" },
})),
...(served ? { q4_records: { slices: 3, with_candidate: 0, with_merge_report: 0, repo_context: "INDETERMINATE", basis: RECORDS } } : {}),
q5_park: [],
} as NonNullable<FleetSnapshot["execution"]>;
const template = demoSnapshot().scopes![0]!.slices[0]!;
const scopes = [{ mission: "simple-flow", slices: ids.map((id, i) => ({ ...template, id, dirName: dir(i), displayName: `Slice ${i + 1}`, status: WORDS[i], proof: { paired: 0, total: 0 } })) }];
return { execution, scopes };
}
const text = (lines: ReturnType<typeof executionContentLines>) => lines.map((line) => line.text).join("\n");
describe("#2761 execution view on the default flow", () => {
it("reads every done-bucket word as declared done, not planned", () => {
const { execution, scopes } = finished(true);
const body = text(executionContentLines(execution, scopes, [], null, 160));
expect(body).toContain("WAVE w1 · 3 slices · 3 declared done");
expect(body).not.toContain("planned");
});
it("the slice strip has nothing next for a slice declared shipped", () => {
const { execution } = finished(true);
const strip = text(executionSliceStripLines(execution, "SF.1", "01-slice", 200, "shipped-to-release-1"));
expect(strip).toContain("next none — declared shipped-to-release-1");
});
it("the evidence page says once that the mission records no builds", () => {
const { execution, scopes } = finished(true);
const page = text(executionContentLines(execution, scopes, [], "evidence", 400));
expect(page).toContain(`records: ${RECORDS}`);
});
it("an older daemon that serves no bucket keeps reading only the word done as declared done", () => {
const { execution, scopes } = finished(false);
const body = text(executionContentLines(execution, scopes, [], null, 160));
expect(body).toContain("WAVE w1 · 3 slices · 3 planned");
expect(text(executionContentLines(execution, scopes, [], "evidence", 400))).not.toContain("records:");
});
});