toolroll 0.9.71 → 0.9.73

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 (131) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/dist/agent-fence.d.ts +10 -2
  3. package/dist/agent-fence.js +47 -9
  4. package/dist/approval-authority.d.ts +20 -0
  5. package/dist/approval-authority.js +67 -0
  6. package/dist/assignment.js +26 -4
  7. package/dist/browser/workspace.css +1 -1
  8. package/dist/browser/workspace.js +28 -28
  9. package/dist/builder.js +48 -6
  10. package/dist/chat-actions.js +13 -8
  11. package/dist/chat-decide.d.ts +47 -1
  12. package/dist/chat-decide.js +186 -5
  13. package/dist/check-access.js +2 -2
  14. package/dist/check-report.d.ts +109 -0
  15. package/dist/check-report.js +205 -0
  16. package/dist/claim.js +1 -1
  17. package/dist/coding-provider.js +4 -2
  18. package/dist/commit-extract.d.ts +111 -0
  19. package/dist/commit-extract.js +514 -0
  20. package/dist/contracts/console-api.d.ts +4 -4
  21. package/dist/contracts/console-api.js +5 -4
  22. package/dist/contracts/flow.d.ts +16 -0
  23. package/dist/contracts/flow.js +3 -0
  24. package/dist/contracts/release-check.d.ts +335 -0
  25. package/dist/contracts/release-check.js +159 -0
  26. package/dist/contracts/release-terms.d.ts +66 -0
  27. package/dist/contracts/release-terms.js +74 -0
  28. package/dist/contracts/verification-receipt.d.ts +42 -8
  29. package/dist/contracts/verification-receipt.js +24 -7
  30. package/dist/contracts/verification-view.d.ts +1 -0
  31. package/dist/deploy-gate.d.ts +17 -0
  32. package/dist/deploy-gate.js +68 -0
  33. package/dist/dispatch.js +3 -10
  34. package/dist/dispose.js +4 -1
  35. package/dist/evidence-pack.d.ts +1 -1
  36. package/dist/evidence-pack.js +20 -2
  37. package/dist/evidence.d.ts +1 -0
  38. package/dist/evidence.js +2 -1
  39. package/dist/exec.d.ts +1 -1
  40. package/dist/exec.js +7 -3
  41. package/dist/flow-engine.js +5 -1
  42. package/dist/flow-gallery-ui.js +4 -2
  43. package/dist/flow-gallery.d.ts +3 -0
  44. package/dist/flow-gallery.js +6 -2
  45. package/dist/flow-pull-request.d.ts +5 -2
  46. package/dist/flow-pull-request.js +14 -5
  47. package/dist/flow-schedule.js +25 -6
  48. package/dist/flow-triggers.d.ts +8 -0
  49. package/dist/flow-triggers.js +15 -0
  50. package/dist/flows-cli.js +13 -3
  51. package/dist/flows-ui.js +7 -1
  52. package/dist/guides.js +10 -1
  53. package/dist/invoke.d.ts +17 -6
  54. package/dist/invoke.js +48 -18
  55. package/dist/kits-ui.js +4 -2
  56. package/dist/kits.d.ts +6 -4
  57. package/dist/kits.js +9 -11
  58. package/dist/lead-status.js +5 -2
  59. package/dist/live-terminal.d.ts +10 -6
  60. package/dist/live-terminal.js +55 -14
  61. package/dist/mcp-connect.d.ts +10 -0
  62. package/dist/mcp-connect.js +2 -0
  63. package/dist/nightly-check.d.ts +104 -0
  64. package/dist/nightly-check.js +362 -0
  65. package/dist/operate.js +39 -2
  66. package/dist/person-proof.d.ts +2 -0
  67. package/dist/person-proof.js +14 -0
  68. package/dist/plan-auto.js +4 -4
  69. package/dist/planner-source.d.ts +1 -1
  70. package/dist/project-delete-ui.js +1 -1
  71. package/dist/project-tools.js +3 -1
  72. package/dist/proof.js +2 -0
  73. package/dist/protected-storage.d.ts +67 -0
  74. package/dist/protected-storage.js +398 -0
  75. package/dist/publish.js +156 -144
  76. package/dist/pull-request-flow.d.ts +2 -0
  77. package/dist/pull-request-flow.js +24 -2
  78. package/dist/release-adapters.d.ts +16 -1
  79. package/dist/release-adapters.js +292 -29
  80. package/dist/release-authority.d.ts +118 -0
  81. package/dist/release-authority.js +135 -0
  82. package/dist/release-card.d.ts +76 -0
  83. package/dist/release-card.js +252 -0
  84. package/dist/release-carry.d.ts +236 -0
  85. package/dist/release-carry.js +664 -0
  86. package/dist/release-cli.d.ts +3 -0
  87. package/dist/release-cli.js +158 -6
  88. package/dist/release-coverage.js +3 -0
  89. package/dist/release-git.d.ts +43 -0
  90. package/dist/release-git.js +106 -0
  91. package/dist/release-recheck.d.ts +146 -0
  92. package/dist/release-recheck.js +657 -0
  93. package/dist/release-view.d.ts +34 -0
  94. package/dist/release-view.js +27 -0
  95. package/dist/release.d.ts +139 -3
  96. package/dist/release.js +641 -250
  97. package/dist/remote-main.d.ts +2 -0
  98. package/dist/remote-main.js +1 -1
  99. package/dist/result-completion.d.ts +6 -0
  100. package/dist/result-completion.js +15 -1
  101. package/dist/result-follow-ups.js +29 -1
  102. package/dist/sandbox-runtime.d.ts +16 -5
  103. package/dist/sandbox-runtime.js +19 -6
  104. package/dist/scope.d.ts +122 -5
  105. package/dist/scope.js +236 -7
  106. package/dist/server/flows.js +40 -10
  107. package/dist/server/render-settings.js +1 -1
  108. package/dist/server/render-tasks.d.ts +8 -0
  109. package/dist/server/render-tasks.js +50 -9
  110. package/dist/server/route-table.js +1 -0
  111. package/dist/server/settings.js +26 -5
  112. package/dist/server/task-views.js +4 -0
  113. package/dist/server/tasks.js +12 -0
  114. package/dist/setup-guide.d.ts +2 -2
  115. package/dist/store.d.ts +230 -5
  116. package/dist/store.js +733 -37
  117. package/dist/subagent-memory.d.ts +1 -0
  118. package/dist/subagent-memory.js +1 -1
  119. package/dist/subagent-tools.d.ts +22 -6
  120. package/dist/subagent-tools.js +44 -8
  121. package/dist/subagent-work.d.ts +2 -0
  122. package/dist/subagent-work.js +5 -2
  123. package/dist/subagents-ui.js +6 -4
  124. package/dist/task-outcome-cli.js +7 -3
  125. package/dist/telegram.d.ts +6 -1
  126. package/dist/telegram.js +58 -6
  127. package/dist/toolroll-update.js +2 -2
  128. package/dist/verification-evidence.d.ts +25 -1
  129. package/dist/verification-evidence.js +57 -2
  130. package/dist/work-index.js +3 -6
  131. package/package.json +1 -1
@@ -0,0 +1,657 @@
1
+ /**
2
+ * Re-running only what failed, by file (docs/plans/release-once.md, part 2).
3
+ *
4
+ * On the second and later checks of a release generation, a check runs only what failed at the newest FULL check A of
5
+ * that generation — the failed unit files, the failed journey groups, the failed upgrade versions — plus every test
6
+ * changed since A and a sanity pass; never the whole suite.
7
+ *
8
+ * - Every outcome at A has a disposition (requiredSet): a passed, unchanged item carries from A; a failed or flaky one
9
+ * re-runs (each failed identity must pass); a part whose results can't be attributed (it stopped, it never said, a
10
+ * worker crashed, only the browser-error check failed) re-runs whole; a failed typecheck or build means a full check.
11
+ * - Obligations are immutable: R(A) comes only from A's sealed evidence. Each targeted attempt seals an obligation
12
+ * snapshot of the earlier targeted runs it took into account and what failed in them; a targeted run C runs R(A),
13
+ * every identity that failed in the snapshot, every test changed since A and the sanity pass — never borrowing
14
+ * coverage from an earlier targeted run.
15
+ * - Coverage is conserved (coverageOf): every item of the new run's inventory has either an eligible pass at A and is
16
+ * unchanged, or a qualifying pass now; otherwise the result is `partial`, which never passes. A required identity
17
+ * that is gone or newly skipped — in a targeted run or in the full run after it — is `identity-removed`: the release
18
+ * stops carrying and a person approves it again.
19
+ *
20
+ * Coverage trusts test code (Alex, 2026-10-25): these results protect against accidental damage, not a hostile test.
21
+ * Authority never reads them; the release's own records do.
22
+ */
23
+ import { CHECK_CONTEXT_CAPTURE, CHECK_INVENTORY_CAPTURE, CHECK_REPORT_CAPTURE, OBLIGATION_SNAPSHOT_CAPTURE, TARGETED_PLAN_CAPTURE, checkContextOf, isUnitFile, payloadDigest, projectGitDir, readSealedJson, treeFactsOf } from "./check-report.js";
24
+ import { readVerifiedCommit, verifiedTreeDiff } from "./commit-extract.js";
25
+ import { checkContextSchema, fileIdentity, groupIdentity, journeyIdentity, obligationSnapshotSchema, readCheckReport, sealedInventorySchema, targetedPlanSchema, unitIdentity, versionIdentity, } from "./contracts/release-check.js";
26
+ import { parseContract } from "./contracts/contract.js";
27
+ import { releaseAuthority, releaseAuthorityOfTask, releaseCarries } from "./release-authority.js";
28
+ import { liveReleaseOf } from "./scope.js";
29
+ import { checkAttemptOf } from "./store.js";
30
+ /** The sanity pass every targeted check runs beside its plan: a few fast tests of the core rules, when the tree has them. */
31
+ export const SANITY_UNITS = Object.freeze(["src/scope.test.ts", "src/contract.test.ts", "src/check-report.test.ts", "src/update-safe-migrations.test.ts"]);
32
+ export function sealedRun(store, root, run) {
33
+ const attempt = checkAttemptOf(store, run);
34
+ if (attempt === null)
35
+ return null;
36
+ const read = (capture, parse) => {
37
+ const sealed = readSealedJson(store, root, run, capture);
38
+ if (sealed === null)
39
+ return { value: null, digest: null };
40
+ return { value: parse(sealed.value), digest: sealed.digest };
41
+ };
42
+ const report = read(CHECK_REPORT_CAPTURE, value => { const r = readCheckReport(value); return r.ok ? r.value : null; });
43
+ const inventory = read(CHECK_INVENTORY_CAPTURE, value => { const r = parseContract(sealedInventorySchema, value); return r.ok ? r.value : null; });
44
+ const context = read(CHECK_CONTEXT_CAPTURE, value => { const r = parseContract(checkContextSchema, value); return r.ok ? r.value : null; });
45
+ const plan = read(TARGETED_PLAN_CAPTURE, value => { const r = parseContract(targetedPlanSchema, value); return r.ok ? r.value : null; });
46
+ const snapshot = read(OBLIGATION_SNAPSHOT_CAPTURE, value => { const r = parseContract(obligationSnapshotSchema, value); return r.ok ? r.value : null; });
47
+ return { run, attempt, report: report.value, reportDigest: report.value === null ? null : report.digest, inventory: inventory.value, context: context.value,
48
+ plan: plan.value, planDigest: plan.value === null ? null : payloadDigest(plan.value), snapshot: snapshot.value, snapshotDigest: snapshot.value === null ? null : payloadDigest(snapshot.value) };
49
+ }
50
+ const label = (script, kind) => kind === "real" ? `${script}-real` : script;
51
+ const JOURNEY_PARTS = [
52
+ { label: "flows", script: "flows", kind: "scripted" }, { label: "app", script: "app", kind: "scripted" },
53
+ { label: "flows-real", script: "flows", kind: "real" }, { label: "app-real", script: "app", kind: "real" },
54
+ ];
55
+ /** The disposition of every outcome at A (the table in the plan). */
56
+ export function requiredSet(a) {
57
+ const none = (full) => ({ full, units: [], parts: [], groups: [], versions: [], required: [], skipped: [] });
58
+ const report = a.report, inventory = a.inventory;
59
+ if (report === null)
60
+ return none("the full check gave no report");
61
+ if (inventory === null)
62
+ return none("the full check's inventory can't be read");
63
+ const part = (name) => report.parts.find(one => one.part === name);
64
+ for (const name of ["typecheck", "build"]) {
65
+ const one = part(name);
66
+ if (one === undefined || !one.ran || one.code !== 0)
67
+ return none(`the ${name} failed`);
68
+ }
69
+ const units = new Set(), parts = new Set(), versions = new Set(), required = new Set(), skipped = new Set();
70
+ const groups = new Map();
71
+ // Units.
72
+ const unitPart = part("unit");
73
+ if (unitPart?.selected === true) {
74
+ const u = report.units;
75
+ if (u === null || !unitPart.ran || u.reusedFromCI || !u.finished || u.runErrors > 0)
76
+ parts.add("unit");
77
+ else {
78
+ const byFile = new Map(u.files.map(one => [one.file, one]));
79
+ // Stopped or not run: an inventory file with no outcome re-runs the whole part.
80
+ if (inventory.sealed.units.some(file => !byFile.has(file)))
81
+ parts.add("unit");
82
+ for (const file of u.files) {
83
+ const failedHere = u.identities.filter(one => one.file === file.file && one.state === "failed");
84
+ if (file.state === "failed") {
85
+ units.add(file.file);
86
+ if (failedHere.length === 0)
87
+ required.add(fileIdentity(file.file));
88
+ for (const one of failedHere)
89
+ required.add(unitIdentity(one.file, one.name));
90
+ }
91
+ if (file.flaky)
92
+ units.add(file.file);
93
+ }
94
+ for (const one of u.identities) {
95
+ if (one.state === "skipped")
96
+ skipped.add(unitIdentity(one.file, one.name));
97
+ if (one.flaky)
98
+ units.add(one.file);
99
+ }
100
+ }
101
+ }
102
+ // Journeys: whole groups.
103
+ for (const lane of JOURNEY_PARTS) {
104
+ const selected = part(lane.label);
105
+ if (selected?.selected !== true)
106
+ continue;
107
+ const run = report.journeys.find(one => one.script === lane.script && one.kind === lane.kind);
108
+ if (!selected.ran || run === undefined || !run.finished || run.groups.some(one => one.state === "not-run")) {
109
+ parts.add(lane.label);
110
+ continue;
111
+ }
112
+ for (const group of run.groups) {
113
+ for (const journey of group.journeys)
114
+ if (journey.state === "skipped")
115
+ skipped.add(journeyIdentity(lane.script, lane.kind, group.group, journey.name));
116
+ if (group.state === "passed" && !group.retried && !group.unattributed)
117
+ continue;
118
+ const key = `${lane.script}:${lane.kind}`;
119
+ const entry = groups.get(key) ?? { script: lane.script, kind: lane.kind, groups: new Set() };
120
+ entry.groups.add(group.group);
121
+ groups.set(key, entry);
122
+ required.add(groupIdentity(lane.script, lane.kind, group.group));
123
+ for (const journey of group.journeys)
124
+ if (journey.state === "failed")
125
+ required.add(journeyIdentity(lane.script, lane.kind, group.group, journey.name));
126
+ }
127
+ }
128
+ // Upgrades: whole versions.
129
+ const upgradePart = part("upgrade");
130
+ if (upgradePart?.selected === true) {
131
+ if (!upgradePart.ran || report.upgrades === null || !report.upgrades.finished)
132
+ parts.add("upgrade");
133
+ else
134
+ for (const one of report.upgrades.versions)
135
+ if (one.state !== "passed") {
136
+ versions.add(one.version);
137
+ required.add(versionIdentity(one.version));
138
+ }
139
+ }
140
+ return {
141
+ full: null, units: [...units].sort(), parts: [...parts].sort(), versions: [...versions].sort(), required: [...required].sort(), skipped: [...skipped].sort(),
142
+ groups: [...groups.values()].map(one => ({ script: one.script, kind: one.kind, groups: [...one.groups].sort() })),
143
+ };
144
+ }
145
+ /** The identities that failed in one run's sealed report (for an obligation snapshot). */
146
+ export function failedIdentities(report) {
147
+ if (report === null)
148
+ return [];
149
+ const out = new Set();
150
+ for (const one of report.units?.identities ?? [])
151
+ if (one.state === "failed")
152
+ out.add(unitIdentity(one.file, one.name));
153
+ for (const file of report.units?.files ?? [])
154
+ if (file.state === "failed" && !(report.units?.identities ?? []).some(one => one.file === file.file && one.state === "failed"))
155
+ out.add(fileIdentity(file.file));
156
+ for (const run of report.journeys)
157
+ for (const group of run.groups) {
158
+ if (group.state === "failed" || group.unattributed)
159
+ out.add(groupIdentity(run.script, run.kind, group.group));
160
+ for (const journey of group.journeys)
161
+ if (journey.state === "failed")
162
+ out.add(journeyIdentity(run.script, run.kind, group.group, journey.name));
163
+ }
164
+ for (const one of report.upgrades?.versions ?? [])
165
+ if (one.state === "failed")
166
+ out.add(versionIdentity(one.version));
167
+ return [...out].sort();
168
+ }
169
+ /** The base A of a generation: the newest run with intended mode full before `before`, if it can be one. */
170
+ function baseOf(store, root, authority, before) {
171
+ const row = store.handle.prepare("SELECT run FROM release_check_attempt WHERE authority = ? AND mode = 'full' AND run < ? ORDER BY run DESC LIMIT 1").get(authority, before);
172
+ if (row === undefined)
173
+ return { problem: "this release has no full check to start from" };
174
+ const base = sealedRun(store, root, Number(row["run"]));
175
+ if (base === null || base.report === null)
176
+ return { problem: "the last full check of this release gave no report" };
177
+ if (base.report.mode !== "full" || base.report.units?.reusedFromCI === true)
178
+ return { problem: "the last full check reused CI results, so it can't be a base" };
179
+ const obligations = requiredSet(base);
180
+ if (obligations.full !== null)
181
+ return { problem: obligations.full };
182
+ return { base, obligations };
183
+ }
184
+ /** The earlier targeted runs of this generation after A, with their sealed report digests and what failed in them. */
185
+ export function obligationSnapshot(store, root, authority, base, before) {
186
+ const rows = store.handle.prepare("SELECT run FROM release_check_attempt WHERE authority = ? AND mode = 'targeted' AND run > ? AND run < ? ORDER BY run").all(authority, base, before);
187
+ return { version: 1, authority, base, earlier: rows.map(row => {
188
+ const sealed = sealedRun(store, root, Number(row["run"]));
189
+ // A targeted run with no readable report proves nothing passed: everything it was asked to run stays owed.
190
+ const failed = sealed?.report == null ? [...(sealed?.plan?.required ?? [])] : failedIdentities(sealed.report);
191
+ return { run: Number(row["run"]), reportDigest: sealed?.reportDigest ?? null, failed };
192
+ }) };
193
+ }
194
+ /** The plan a targeted run runs: R(A), the snapshot's failures, every test changed since A, and the sanity pass. */
195
+ export function planFrom(base, obligations, snapshot, changed, units, journeysRan) {
196
+ const files = new Set(obligations.units);
197
+ const parts = new Set(obligations.parts);
198
+ const groups = new Map(obligations.groups.map(one => [`${one.script}:${one.kind}`, { ...one, groups: new Set(one.groups) }]));
199
+ const versions = new Set(obligations.versions);
200
+ const required = new Set(obligations.required);
201
+ for (const earlier of snapshot.earlier)
202
+ for (const identity of earlier.failed) {
203
+ required.add(identity);
204
+ const unit = /^unit:(.+?)::/.exec(identity)?.[1] ?? /^file:(.+)$/.exec(identity)?.[1];
205
+ if (unit !== undefined)
206
+ files.add(unit);
207
+ const group = /^(?:group|journey):(app|flows):(scripted|real):([^:]+?)(?:::|$)/.exec(identity);
208
+ if (group !== null) {
209
+ const key = `${group[1]}:${group[2]}`;
210
+ const entry = groups.get(key) ?? { script: group[1], kind: group[2], groups: new Set() };
211
+ entry.groups.add(group[3]);
212
+ groups.set(key, entry);
213
+ }
214
+ const version = /^upgrade:(.+)$/.exec(identity)?.[1];
215
+ if (version !== undefined)
216
+ versions.add(version);
217
+ }
218
+ for (const path of changed) {
219
+ if (isUnitFile(path))
220
+ files.add(path);
221
+ // A changed journey script runs all its groups again: its whole part, as A ran it.
222
+ if (path === "scripts/app-e2e.mjs" || path === "scripts/flows-e2e.mjs") {
223
+ const script = path.includes("app") ? "app" : "flows";
224
+ for (const lane of JOURNEY_PARTS)
225
+ if (lane.script === script && journeysRan.includes(lane.label))
226
+ parts.add(lane.label);
227
+ }
228
+ }
229
+ return {
230
+ version: 1, base, units: [...files].filter(file => units.includes(file)).sort(), sanity: SANITY_UNITS.filter(file => units.includes(file) && !files.has(file)),
231
+ groups: [...groups.values()].filter(one => !parts.has(label(one.script, one.kind))).map(one => ({ script: one.script, kind: one.kind, groups: [...one.groups].sort() })),
232
+ versions: parts.has("upgrade") ? [] : [...versions].sort(), parts: [...parts].sort(), required: [...required].sort(),
233
+ };
234
+ }
235
+ /**
236
+ * Whether the check about to run for this release task may be targeted (planTargetedRecheck): only when A and this run
237
+ * are gate tasks of the same current generation, A is the generation's newest full check with a valid report that
238
+ * accounts for every item and reused nothing from CI, everything since A is test-only (the carry proved it), the
239
+ * execution context matches A's, and no main-check failure is open. Otherwise a full check, told which identities it
240
+ * must still report (watch).
241
+ */
242
+ export function planTargetedRecheck(store, root, input) {
243
+ // The nightly's open failures are reported by name by every full check, so a pass can clear exactly them.
244
+ const mainOpen = store.handle.prepare("SELECT DISTINCT item FROM main_check_failure WHERE repo = ? AND cleared_at IS NULL").all(input.repo).map(row => String(row["item"]));
245
+ const found = releaseAuthorityOfTask(store, input.taskId);
246
+ if (found === null)
247
+ return { mode: "full", authority: null, why: "not a release task", watch: mainOpen.sort() };
248
+ const a = found.authority;
249
+ const owed = owedIdentities(store, root, a.id, input.runId);
250
+ const full = (why, watch = []) => ({ mode: "full", authority: a.id, why, watch: [...new Set([...watch, ...owed, ...mainOpen])].sort() });
251
+ const live = liveReleaseOf(store, input.taskId, "dispatch", input.now);
252
+ if (!live.applies || !live.ok)
253
+ return full("the release approval doesn't stand");
254
+ const based = baseOf(store, root, a.id, input.runId);
255
+ if ("problem" in based)
256
+ return full(based.problem);
257
+ const { base, obligations } = based;
258
+ const snapshot = obligationSnapshot(store, root, a.id, base.run, input.runId);
259
+ const watch = [...new Set([...obligations.required, ...snapshot.earlier.flatMap(one => one.failed)])].sort();
260
+ const open = Number(store.handle.prepare("SELECT COUNT(*) AS n FROM main_check_failure WHERE repo = ? AND cleared_at IS NULL").get(a.repo)?.["n"] ?? 0);
261
+ if (open > 0)
262
+ return full("the nightly full check on main has open failures", watch);
263
+ if (base.context === null || base.inventory === null)
264
+ return full("the last full check's context can't be read", watch);
265
+ // Everything since A, read from verified objects: only tests may differ (a carry proved the rest).
266
+ const gitDir = projectGitDir(input.repo);
267
+ const baseHead = store.getRun(base.run)?.headRevision ?? null;
268
+ if (gitDir === null || baseHead === null)
269
+ return full("the release's commits can't be read", watch);
270
+ const from = readVerifiedCommit(gitDir, baseHead), to = readVerifiedCommit(gitDir, input.head);
271
+ if (!from.ok || !to.ok)
272
+ return full("the release's commits can't be read", watch);
273
+ const diff = verifiedTreeDiff(gitDir, from.value.tree, to.value.tree);
274
+ if (!diff.ok)
275
+ return full("the change since the last full check can't be read", watch);
276
+ const changed = diff.value.map(one => one.path);
277
+ if (diff.value.some(one => one.kind === "deleted" || one.binary))
278
+ return full("a file was removed since the last full check", watch);
279
+ if (changed.some(path => !(isUnitFile(path) || path === "scripts/app-e2e.mjs" || path === "scripts/flows-e2e.mjs" || path.endsWith(".md"))))
280
+ return full("more than tests changed since the last full check", watch);
281
+ if (found.carry === null && input.taskId !== a.anchorTask)
282
+ return full("this task isn't part of the release", watch);
283
+ // The context must match A's exactly (the harness digest leaves the tests themselves out).
284
+ const facts = treeFactsOf(gitDir, input.head);
285
+ if (!facts.ok)
286
+ return full("this commit can't be read", watch);
287
+ if (payloadDigest(checkContextOf(facts.facts, input.command, input.access)) !== payloadDigest(base.context))
288
+ return full("the check runs somewhere other than the last full check did", watch);
289
+ if (base.inventory.sealed.units.some(file => !facts.facts.units.includes(file)))
290
+ return full("a unit file the last full check had is gone", watch);
291
+ const journeysRan = base.report.parts.filter(one => one.ran).map(one => one.part);
292
+ const plan = planFrom(base.run, { ...obligations, required: [...new Set([...obligations.required, ...owed])].sort(),
293
+ units: [...new Set([...obligations.units, ...owed.flatMap(id => /^unit:(.+?)::/.exec(id)?.[1] ?? /^file:(.+)$/.exec(id)?.[1] ?? [])])].sort() }, snapshot, changed, facts.facts.units, journeysRan);
294
+ return { mode: "targeted", authority: a.id, plan, snapshot, watch: plan.required };
295
+ }
296
+ /**
297
+ * Whether a targeted run B, with A's sealed evidence, its plan and snapshot, covers every item of B's inventory
298
+ * (coverage conservation), and every required identity passed — re-derived from sealed values only.
299
+ */
300
+ export function coverageOf(input) {
301
+ const { a, b, plan, snapshot } = input;
302
+ const obligations = requiredSet(a);
303
+ if (obligations.full !== null)
304
+ return "partial";
305
+ // The plan holds everything A and the snapshot owe: a plan that leaves one out is no plan for this base.
306
+ const owed = new Set([...obligations.required, ...snapshot.earlier.flatMap(one => one.failed)]);
307
+ if ([...owed].some(identity => !plan.required.includes(identity)))
308
+ return "partial";
309
+ if (obligations.units.some(file => !plan.units.includes(file)) || obligations.parts.some(part => !plan.parts.includes(part)))
310
+ return "partial";
311
+ if (b.report === null || b.inventory === null || b.report.mode !== "targeted" || b.report.planDigest !== payloadDigest(plan))
312
+ return "partial";
313
+ const report = b.report;
314
+ for (const name of ["typecheck", "build"]) {
315
+ const one = report.parts.find(p => p.part === name);
316
+ if (one === undefined || !one.ran || one.code !== 0)
317
+ return "partial";
318
+ }
319
+ // Required identities: reported, and passed. Gone, or newly skipped, asks again.
320
+ const passed = new Set(), skippedNow = new Set(), reported = new Set();
321
+ for (const one of report.units?.identities ?? []) {
322
+ const id = unitIdentity(one.file, one.name);
323
+ reported.add(id);
324
+ if (one.state === "passed")
325
+ passed.add(id);
326
+ if (one.state === "skipped")
327
+ skippedNow.add(id);
328
+ }
329
+ for (const file of report.units?.files ?? []) {
330
+ reported.add(fileIdentity(file.file));
331
+ if (file.state === "passed")
332
+ passed.add(fileIdentity(file.file));
333
+ }
334
+ for (const run of report.journeys)
335
+ for (const group of run.groups) {
336
+ const gid = groupIdentity(run.script, run.kind, group.group);
337
+ reported.add(gid);
338
+ if (group.state === "passed" && !group.unattributed)
339
+ passed.add(gid);
340
+ for (const journey of group.journeys) {
341
+ const id = journeyIdentity(run.script, run.kind, group.group, journey.name);
342
+ reported.add(id);
343
+ if (journey.state === "passed")
344
+ passed.add(id);
345
+ if (journey.state === "skipped")
346
+ skippedNow.add(id);
347
+ }
348
+ }
349
+ for (const one of report.upgrades?.versions ?? []) {
350
+ reported.add(versionIdentity(one.version));
351
+ if (one.state === "passed")
352
+ passed.add(versionIdentity(one.version));
353
+ }
354
+ const ranFiles = new Set((report.units?.files ?? []).map(one => one.file));
355
+ for (const identity of plan.required) {
356
+ const unitFile = /^unit:(.+?)::/.exec(identity)?.[1];
357
+ const ran = unitFile === undefined || ranFiles.has(unitFile);
358
+ if (ran && (!reported.has(identity) || skippedNow.has(identity)))
359
+ return "identity-removed";
360
+ }
361
+ for (const id of skippedNow)
362
+ if (!obligations.skipped.includes(id))
363
+ return "identity-removed";
364
+ if (plan.required.some(identity => !passed.has(identity)))
365
+ return "partial";
366
+ if (input.b.exitCode !== 0)
367
+ return "partial";
368
+ // Every owed whole part ran whole and finished, its inventory complete and every item passed (review round 1).
369
+ for (const owedPart of plan.parts) {
370
+ const ran = report.parts.find(one => one.part === owedPart);
371
+ if (ran === undefined || !ran.ran || ran.code !== 0)
372
+ return "partial";
373
+ if (owedPart === "unit") {
374
+ const u = report.units;
375
+ if (u === null || u.reusedFromCI || !u.finished || u.runErrors > 0)
376
+ return "partial";
377
+ const states = new Map(u.files.map(one => [one.file, one.state]));
378
+ if (b.inventory.sealed.units.some(file => states.get(file) === undefined || states.get(file) === "failed"))
379
+ return "partial";
380
+ }
381
+ else if (owedPart === "upgrade") {
382
+ const up = report.upgrades;
383
+ if (up === null || !up.finished || up.versions.length === 0 || up.versions.some(one => one.state !== "passed"))
384
+ return "partial";
385
+ const versions = new Set(up.versions.map(one => one.version));
386
+ if (a.inventory.declared.versions.some(version => !versions.has(version)))
387
+ return "partial";
388
+ }
389
+ else if (owedPart !== "typecheck" && owedPart !== "build") {
390
+ const lane = JOURNEY_PARTS.find(one => one.label === owedPart);
391
+ const run = report.journeys.find(one => one.script === lane.script && one.kind === lane.kind);
392
+ if (run === undefined || !run.finished || run.groups.length === 0 || run.groups.some(one => one.state !== "passed" || one.unattributed))
393
+ return "partial";
394
+ const declared = a.inventory.declared.groups.find(one => one.script === lane.script && one.kind === lane.kind)?.groups ?? [];
395
+ const groups = new Set(run.groups.map(one => one.group));
396
+ if (declared.some(group => !groups.has(group)))
397
+ return "partial";
398
+ }
399
+ }
400
+ // Conservation, unit files: passed at A (not flaky) and not owed again, or passed here.
401
+ const aFiles = new Map((a.report.units?.files ?? []).map(one => [one.file, one]));
402
+ const bFiles = new Map((report.units?.files ?? []).map(one => [one.file, one]));
403
+ const rerun = new Set([...plan.units, ...plan.sanity]);
404
+ if (a.inventory.sealed.units.some(file => !b.inventory.sealed.units.includes(file)))
405
+ return "identity-removed";
406
+ for (const file of b.inventory.sealed.units) {
407
+ const here = bFiles.get(file);
408
+ const before = aFiles.get(file);
409
+ const eligible = !plan.parts.includes("unit") && !rerun.has(file) && before !== undefined && before.state !== "failed" && !before.flaky;
410
+ if (!eligible && !(here !== undefined && here.state !== "failed"))
411
+ return "partial";
412
+ }
413
+ // Journey groups and upgrade versions: passed at A and not owed, or passed here.
414
+ for (const run of a.report.journeys)
415
+ for (const group of run.groups) {
416
+ const owedHere = plan.parts.includes(label(run.script, run.kind)) || plan.groups.some(one => one.script === run.script && one.kind === run.kind && one.groups.includes(group.group));
417
+ const passedAtA = group.state === "passed" && !group.retried && !group.unattributed;
418
+ if (owedHere || !passedAtA) {
419
+ if (!passed.has(groupIdentity(run.script, run.kind, group.group)))
420
+ return "partial";
421
+ }
422
+ }
423
+ for (const one of a.report.upgrades?.versions ?? []) {
424
+ const owedHere = plan.parts.includes("upgrade") || plan.versions.includes(one.version) || one.state !== "passed";
425
+ if (owedHere && !passed.has(versionIdentity(one.version)))
426
+ return "partial";
427
+ }
428
+ return "composed";
429
+ }
430
+ /** A full run's own check: a required identity of the generation that is gone or newly skipped. */
431
+ export function fullRunRemovedIdentity(report, watch, skippedAtBase) {
432
+ if (report === null || watch.length === 0)
433
+ return false;
434
+ const reported = new Map();
435
+ for (const one of report.units?.identities ?? [])
436
+ reported.set(unitIdentity(one.file, one.name), one.state);
437
+ for (const file of report.units?.files ?? [])
438
+ reported.set(fileIdentity(file.file), file.state);
439
+ for (const run of report.journeys)
440
+ for (const group of run.groups) {
441
+ reported.set(groupIdentity(run.script, run.kind, group.group), group.state);
442
+ for (const journey of group.journeys)
443
+ reported.set(journeyIdentity(run.script, run.kind, group.group, journey.name), journey.state);
444
+ }
445
+ for (const one of report.upgrades?.versions ?? [])
446
+ reported.set(versionIdentity(one.version), one.state);
447
+ const ranFiles = new Set((report.units?.files ?? []).map(one => one.file));
448
+ return watch.some(identity => {
449
+ const unitFile = /^unit:(.+?)::/.exec(identity)?.[1];
450
+ if (unitFile !== undefined && !ranFiles.has(unitFile))
451
+ return true;
452
+ const state = reported.get(identity);
453
+ return state === undefined || (state === "skipped" && !skippedAtBase.includes(identity));
454
+ });
455
+ }
456
+ /**
457
+ * After the check: its coverage, and for a composed one the links its receipt names. An identity-removed result stops
458
+ * the release carrying (a person approves it again). `gateOf` reads a run's verified receipt digest.
459
+ */
460
+ export function settleCoverage(store, root, input, gateOf) {
461
+ const b = sealedRun(store, root, input.runId);
462
+ if (b === null)
463
+ return { coverage: "full", composed: null };
464
+ const removed = () => {
465
+ if (b.attempt.authority !== null) {
466
+ const a = releaseAuthority(store, b.attempt.authority);
467
+ if (a !== null && ["open", "deploying"].includes(a.state)) {
468
+ store.handle.prepare("UPDATE release_authority SET state = 'revoked', ended_at = ?, ended_by = ?, rev = rev + 1 WHERE id = ?").run(input.now.toISOString(), "toolroll: a required test was removed or skipped", a.id);
469
+ store.recordAction({ at: input.now.toISOString(), actor: "toolroll", repo: a.repo, taskId: input.taskId, runId: input.runId, action: "release revoked", outcome: "identity-removed", source: "work",
470
+ detail: `release ${a.id} · run ${input.runId}: a required test was removed, renamed or newly skipped; a person approves this commit` });
471
+ }
472
+ }
473
+ return { coverage: "identity-removed", composed: null };
474
+ };
475
+ if (b.attempt.mode === "full") {
476
+ // The rule on removed or skipped required identities applies to full runs too.
477
+ if (b.attempt.authority !== null) {
478
+ // Owed whatever made the last full check ineligible as a base: a failed typecheck never wipes them.
479
+ const watch = owedIdentities(store, root, b.attempt.authority, input.runId);
480
+ if (fullRunRemovedIdentity(b.report, watch, []))
481
+ return removed();
482
+ // Every identity that ever failed in this generation stays protected, passed since or not (review round 2).
483
+ if (removedProtected(b.report, b.inventory, protectedIdentities(store, root, b.attempt.authority, input.runId)))
484
+ return removed();
485
+ }
486
+ return { coverage: "full", composed: null };
487
+ }
488
+ if (b.plan === null || b.snapshot === null || b.attempt.baseRun === null || b.attempt.authority === null)
489
+ return { coverage: "partial", composed: null };
490
+ const a = sealedRun(store, root, b.attempt.baseRun);
491
+ if (a === null || a.report === null || a.inventory === null)
492
+ return { coverage: "partial", composed: null };
493
+ const coverage = coverageOf({ a: { report: a.report, inventory: a.inventory }, b: { report: b.report, inventory: b.inventory, exitCode: input.exitCode }, plan: b.plan, snapshot: b.snapshot });
494
+ if (coverage === "identity-removed")
495
+ return removed();
496
+ if (removedProtected(b.report, b.inventory, protectedIdentities(store, root, b.attempt.authority, input.runId)))
497
+ return removed();
498
+ if (coverage !== "composed")
499
+ return { coverage, composed: null };
500
+ const authority = releaseAuthority(store, b.attempt.authority);
501
+ const baseReceipt = gateOf(a.run);
502
+ if (authority === null || authority.artifactDigest === null || baseReceipt === null || a.reportDigest === null || b.reportDigest === null)
503
+ return { coverage: "partial", composed: null };
504
+ return { coverage, composed: { base: a.run, baseReceipt, baseReport: a.reportDigest, authority: authority.id, carries: releaseCarries(store, authority.id).filter(one => one.taskId === input.taskId).map(one => one.id),
505
+ artifacts: authority.artifactDigest, plan: b.planDigest, snapshot: b.snapshotDigest, report: b.reportDigest } };
506
+ }
507
+ /**
508
+ * Re-prove every link a composed receipt names, from sealed artifacts and the store: A's receipt and report, this run's
509
+ * plan (re-derived from A's sealed evidence and the sealed snapshot), its snapshot and report, the release's artifact
510
+ * digest on every carry of this task, the generation and the grant. Null when every link holds; else why not.
511
+ */
512
+ export function provedComposition(store, root, runId, links, gateOf, grantOf) {
513
+ const b = sealedRun(store, root, runId);
514
+ if (b === null || b.attempt.mode !== "targeted" || b.attempt.baseRun !== links.base || b.attempt.authority !== links.authority)
515
+ return "The composed check's attempt doesn't match its record.";
516
+ if (b.plan === null || b.planDigest !== links.plan || b.attempt.planDigest !== links.plan)
517
+ return "The composed check's plan changed.";
518
+ if (b.snapshot === null || b.snapshotDigest !== links.snapshot || b.attempt.snapshotDigest !== links.snapshot)
519
+ return "The composed check's obligation snapshot changed.";
520
+ if (b.reportDigest === null || b.reportDigest !== links.report)
521
+ return "The composed check's report changed.";
522
+ const a = sealedRun(store, root, links.base);
523
+ if (a === null || a.report === null || a.inventory === null || a.reportDigest !== links.baseReport || a.attempt.authority !== links.authority || a.attempt.mode !== "full")
524
+ return "The full check it was composed from changed.";
525
+ if (gateOf(links.base) !== links.baseReceipt)
526
+ return "The full check it was composed from no longer verifies.";
527
+ const authority = releaseAuthority(store, links.authority);
528
+ if (authority === null || authority.artifactDigest !== links.artifacts)
529
+ return "The release's production build changed.";
530
+ const carries = releaseCarries(store, links.authority).filter(one => links.carries.includes(one.id));
531
+ if (carries.length !== links.carries.length || carries.some(one => one.artifactDigest !== links.artifacts))
532
+ return "A carried commit's build doesn't match the release's.";
533
+ if (grantOf(links.base) === null || grantOf(links.base) !== grantOf(runId))
534
+ return "The release check command differs from the full check's.";
535
+ const coverage = coverageOf({ a: { report: a.report, inventory: a.inventory }, b: { report: b.report, inventory: b.inventory, exitCode: b.report?.exitCode ?? null }, plan: b.plan, snapshot: b.snapshot });
536
+ if (coverage !== "composed")
537
+ return "The composed check no longer covers what the full check owed.";
538
+ if (removedProtected(b.report, b.inventory, protectedIdentities(store, root, links.authority, runId)))
539
+ return "The composed check removed a test that failed earlier in this release.";
540
+ return null;
541
+ }
542
+ /** The identities a later check of this release still has to run again and pass (the anchor page's obligations). */
543
+ export function releaseObligationsOf(store, root, authority) {
544
+ const newest = store.handle.prepare("SELECT run FROM release_check_attempt WHERE authority = ? ORDER BY run DESC LIMIT 1").get(authority);
545
+ if (newest === undefined)
546
+ return [];
547
+ const sealed = sealedRun(store, root, Number(newest["run"]));
548
+ if (sealed?.report == null)
549
+ return [];
550
+ return sealed.report.exitCode === 0 ? [] : failedIdentities(sealed.report);
551
+ }
552
+ /**
553
+ * The identities a release generation still owes, independent of whether a targeted check is allowed (review round
554
+ * 1): every identity that failed in any of its checks before `before`, until a later check reported it PASSED by name.
555
+ * A failed typecheck or build doesn't wipe them, and a full fallback keeps them: each must be reported again, and a
556
+ * check that drops or newly skips one asks a person again.
557
+ */
558
+ export function owedIdentities(store, root, authority, before) {
559
+ const owed = new Set();
560
+ for (const row of store.handle.prepare("SELECT run FROM release_check_attempt WHERE authority = ? AND run < ? ORDER BY run").all(authority, before)) {
561
+ const sealed = sealedRun(store, root, Number(row["run"]));
562
+ const report = sealed?.report ?? null;
563
+ if (report === null)
564
+ continue;
565
+ for (const identity of passedIdentities(report))
566
+ owed.delete(identity);
567
+ for (const identity of failedIdentities(report))
568
+ owed.add(identity);
569
+ }
570
+ return [...owed].sort();
571
+ }
572
+ /**
573
+ * The generation's protected identities (review round 2): every identity that failed in any of its checks before
574
+ * `before`. Never shrinks — a later pass doesn't forgive a later removal: removing, renaming or newly skipping any of
575
+ * them, in any later run of the generation, asks a person again. Separate from what is still owed (owedIdentities).
576
+ */
577
+ export function protectedIdentities(store, root, authority, before) {
578
+ const kept = new Set();
579
+ for (const row of store.handle.prepare("SELECT run FROM release_check_attempt WHERE authority = ? AND run < ? ORDER BY run").all(authority, before)) {
580
+ const report = sealedRun(store, root, Number(row["run"]))?.report ?? null;
581
+ if (report !== null)
582
+ for (const identity of failedIdentities(report))
583
+ kept.add(identity);
584
+ }
585
+ return [...kept].sort();
586
+ }
587
+ /**
588
+ * Whether a run's report removed one of the protected identities: a unit test or journey gone or newly skipped from a
589
+ * file or group the run did report, a unit file gone from its inventory, a group or upgrade version missing from a lane
590
+ * that finished. A lane that didn't run or finish is no pass, so it is judged by the next run that does.
591
+ */
592
+ export function removedProtected(report, inventory, kept) {
593
+ if (report === null || kept.length === 0)
594
+ return false;
595
+ const states = new Map();
596
+ for (const one of report.units?.identities ?? [])
597
+ states.set(unitIdentity(one.file, one.name), one.state);
598
+ const ranFiles = new Set((report.units?.files ?? []).map(one => one.file));
599
+ const units = new Set(inventory?.sealed.units ?? []);
600
+ return kept.some(identity => {
601
+ const unit = /^unit:(.+?)::/.exec(identity);
602
+ if (unit !== null) {
603
+ const file = unit[1];
604
+ if (inventory !== null && !units.has(file))
605
+ return true;
606
+ if (!ranFiles.has(file))
607
+ return false;
608
+ const state = states.get(identity);
609
+ return state === undefined || state === "skipped";
610
+ }
611
+ const file = /^file:(.+)$/.exec(identity)?.[1];
612
+ if (file !== undefined)
613
+ return inventory !== null && !units.has(file);
614
+ const journey = /^(group|journey):(app|flows):(scripted|real):(.+)$/.exec(identity);
615
+ if (journey !== null) {
616
+ const script = journey[2], kind = journey[3];
617
+ const run = report.journeys.find(one => one.script === script && one.kind === kind);
618
+ if (run === undefined || !run.finished)
619
+ return false;
620
+ // A group that ran reports every journey in it; a group missing counts only from a full run (a targeted one
621
+ // runs the groups it owes).
622
+ const group = run.groups.find(one => journey[1] === "group" ? groupIdentity(run.script, run.kind, one.group) === identity : identity.startsWith(`${groupIdentity(run.script, run.kind, one.group)}::`));
623
+ if (group === undefined)
624
+ return report.mode === "full";
625
+ if (journey[1] === "group")
626
+ return false;
627
+ const one = group.journeys.find(j => journeyIdentity(run.script, run.kind, group.group, j.name) === identity);
628
+ return one === undefined || one.state === "skipped";
629
+ }
630
+ const version = /^upgrade:(.+)$/.exec(identity)?.[1];
631
+ if (version !== undefined)
632
+ return report.mode === "full" && report.upgrades !== null && report.upgrades.finished && !report.upgrades.versions.some(one => one.version === version);
633
+ return false;
634
+ });
635
+ }
636
+ /** The identities a report shows passed by name. */
637
+ function passedIdentities(report) {
638
+ const out = [];
639
+ for (const one of report.units?.identities ?? [])
640
+ if (one.state === "passed")
641
+ out.push(unitIdentity(one.file, one.name));
642
+ for (const file of report.units?.files ?? [])
643
+ if (file.state === "passed" && file.failed === 0)
644
+ out.push(fileIdentity(file.file));
645
+ for (const run of report.journeys)
646
+ for (const group of run.groups) {
647
+ if (group.state === "passed" && !group.unattributed)
648
+ out.push(groupIdentity(run.script, run.kind, group.group));
649
+ for (const journey of group.journeys)
650
+ if (journey.state === "passed")
651
+ out.push(journeyIdentity(run.script, run.kind, group.group, journey.name));
652
+ }
653
+ for (const one of report.upgrades?.versions ?? [])
654
+ if (one.state === "passed")
655
+ out.push(versionIdentity(one.version));
656
+ return out;
657
+ }