@deksden-com/dd-flow-cli 0.9.0-beta.120 → 0.9.0-beta.122

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 (38) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/build-info.json +5 -5
  3. package/dist/cli/hook-ingress.js +4 -3
  4. package/dist/cli/native-hook-ingress.js +34 -2
  5. package/dist/harness-runtime/bin/dd-agy.mjs +6 -3
  6. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +16 -0
  7. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +12 -2
  8. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +60 -8
  9. package/dist/harness-runtime/lib/dd-zcode.mjs +28 -13
  10. package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
  11. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  12. package/dist/harness-runtime/lib/lifecycle-command.mjs +218 -0
  13. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  14. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  15. package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
  16. package/dist/harness-runtime/lib/native-hook-command.mjs +36 -5
  17. package/dist/runtime/context.js +1 -1
  18. package/dist/schemas/flow-run.schema.json +1 -0
  19. package/dist/services/code-checks.js +40 -0
  20. package/dist/services/codex-hook-delivery.js +6 -2
  21. package/dist/services/hooks.js +51 -59
  22. package/dist/services/lifecycle-command.js +1 -383
  23. package/dist/services/lifecycle-contract.js +1 -30
  24. package/dist/services/lifecycle-invocations.js +34 -13
  25. package/dist/services/managed-processes.js +42 -6
  26. package/dist/services/run-control.js +7 -1
  27. package/dist/services/runs.js +60 -47
  28. package/dist/services/runtime-scope-stop.js +23 -2
  29. package/dist/services/stage-report-renderer.js +19 -0
  30. package/dist/services/verification-matrix.js +325 -0
  31. package/dist/services/vnext-code-review.js +25 -8
  32. package/dist/services/vnext-code.js +38 -11
  33. package/dist/services/vnext-merge.js +30 -7
  34. package/dist/services/vnext-plan-review.js +22 -6
  35. package/dist/services/vnext-plan.js +45 -17
  36. package/dist/services/work-registry.js +13 -1
  37. package/dist/storage/database.js +43 -9
  38. package/package.json +4 -1
@@ -0,0 +1,325 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { vnextStageDirectory } from "../domain/stage-catalog.js";
6
+ import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations } from "./code-checks.js";
7
+ import { acceptStagePacket, prepareStagePacket, publishStagePacket } from "./stage-report-renderer.js";
8
+ import { assertRunMutationAllowed } from "./run-recovery.js";
9
+ export const verificationMatrixContract = "dd-flow/verification-matrix@1";
10
+ /** A view of already validated facts: never a gate or a receipt selector. */
11
+ export function buildVerificationMatrix(input) {
12
+ const sourceCatalog = new Map();
13
+ for (const source of input.sources) {
14
+ const identity = JSON.stringify([source.role, source.root, source.path]);
15
+ const prior = sourceCatalog.get(identity);
16
+ if (prior && prior.sha256 !== source.sha256)
17
+ throw new AppError("verification_matrix_source_changed", "Matrix source identity has conflicting checksums", 1, { role: source.role, root: source.root, path: source.path, expected: prior.sha256, current: source.sha256 });
18
+ sourceCatalog.set(identity, source);
19
+ }
20
+ const sources = [...sourceCatalog.values()].sort((a, b) => compare(`${a.role}:${a.root}:${a.path}`, `${b.role}:${b.root}:${b.path}`));
21
+ const facts = new Map((input.facts ?? []).map(fact => [fact.canonical_ref, fact]));
22
+ const fact = (check, canonicalRef) => facts.get(canonicalRef) ?? { id: check.id, canonical_ref: canonicalRef, declaration: check, result: null, disposition: input.header.completeness === "skeleton" || input.header.completeness === "declared" ? "not_run" : "not_due" };
23
+ const protocols = Object.keys(input.ownership).sort(compare);
24
+ const requirements = protocols.flatMap(protocolId => input.requirements.filter(item => input.ownership[protocolId].includes(item.id)).map(item => {
25
+ const plan = input.plans.find(value => value.protocol_id === protocolId);
26
+ return { protocol_id: protocolId, ...item, plan_items: (plan?.items ?? []).filter(value => value.requirement_refs.includes(item.id)).map(value => ({ id: value.id, check_refs: unique(value.verification.check_refs), work_ids: unique((input.works ?? []).filter(work => work.protocol_id === protocolId && work.plan_item_id === value.id).map(work => work.work_id)) })).sort((a, b) => compare(a.id, b.id)) };
27
+ })).sort((a, b) => compare(`${a.protocol_id}:${a.id}`, `${b.protocol_id}:${b.id}`));
28
+ const criteria = protocols.flatMap(protocolId => input.criteria.filter(item => input.ownership[protocolId].includes(item.id)).flatMap(item => {
29
+ const plan = input.plans.find(value => value.protocol_id === protocolId);
30
+ const entries = plan?.acceptance.filter(value => value.criterion_id === item.id) ?? [];
31
+ return (entries.length ? entries : [null]).map(entry => ({ protocol_id: protocolId, criterion_id: item.id, statement: item.statement, gate: entry?.gate ?? null, check_refs: unique(entry?.check_refs ?? []), declaration: entry, checks: unique(entry?.check_refs ?? []).map(id => {
32
+ const check = plan?.checks.find(value => value.id === id);
33
+ if (!check)
34
+ throw new AppError("verification_matrix_source_invalid", "Validated PLAN references an absent check", 1, { protocol_id: protocolId, check_id: id });
35
+ return fact(check, `${protocolId}/${id}`);
36
+ }) }));
37
+ })).sort((a, b) => compare(`${a.protocol_id}:${a.criterion_id}:${a.gate}`, `${b.protocol_id}:${b.criterion_id}:${b.gate}`));
38
+ const policyChecks = (input.policyChecks ?? []).map(check => fact(check, check.canonical_ref ?? check.id)).sort((a, b) => compare(a.canonical_ref, b.canonical_ref));
39
+ const projectedFacts = [...new Map([...criteria.flatMap(row => row.checks), ...policyChecks].map(fact => [fact.canonical_ref, fact])).values()].sort((a, b) => compare(a.canonical_ref, b.canonical_ref));
40
+ const descriptor = { header: input.header, sources, facts: projectedFacts };
41
+ return canonical({ schema_id: verificationMatrixContract, header: { ...input.header, source_fingerprint: digest(stableJson(descriptor)) }, sources, requirements, criteria, policy_checks: policyChecks, ...(input.semanticAssessment ? { semantic_assessment: input.semanticAssessment } : {}) });
42
+ }
43
+ export function renderVerificationMatrix(matrix) {
44
+ const h = matrix.header;
45
+ const status = (check) => check.disposition === "unavailable" ? `unavailable (reported native status: ${check.result?.status ?? "none"}; ${check.reason ?? "missing exact proof"})` : String(check.result?.status ?? check.disposition);
46
+ return [`# Verification matrix`, "", `RUN: ${escape(h.run_id)}; stage: ${escape(h.stage)} / ${escape(h.stage_attempt)}; role: ${h.role}; completeness: ${h.completeness}.`, "", "This generated view is not semantic acceptance. A retained passed check proves only its declared contour and proof limits.", "", "## Requirements", "", "| Protocol | Requirement | Statement | PLAN items / checks |", "| --- | --- | --- | --- |", ...matrix.requirements.map(row => `| ${escape(row.protocol_id)} | ${escape(row.id)} | ${escape(row.statement)} | ${escape(row.plan_items.map(item => `${item.id}: ${item.check_refs.join(", ")}`).join("; "))} |`), "", "## Acceptance criteria", "", "| Protocol | Criterion | Gate | Checks / native results | Proof limits |", "| --- | --- | --- | --- | --- |", ...matrix.criteria.map(row => `| ${escape(row.protocol_id)} | ${escape(row.criterion_id)} | ${escape(row.gate ?? "not declared")} | ${escape(row.checks.map(check => `${check.canonical_ref}: ${status(check)}`).join("; "))} | ${escape(JSON.stringify(row.declaration?.proof_limits ?? []))} |`), "", "## Policy checks", "", ...matrix.policy_checks.map(check => `- ${escape(check.canonical_ref)}: ${escape(status(check))}`), "", "## Sources", "", ...matrix.sources.map(source => `- ${escape(source.role)}: ${escape(`${source.root}:${source.path}`)} (${source.sha256})`), ""].join("\n");
47
+ }
48
+ export function verificationMatrixInstruction(metadata, runHome, task) {
49
+ if (!metadata)
50
+ return "";
51
+ return `<verification_matrix>\nRuntime generated the technical verification matrix: ${safePath(runHome, metadata.markdown.path, false)}; structured data: ${safePath(runHome, metadata.json.path, false)}. Source fingerprint: ${metadata.source_fingerprint}; completeness: ${metadata.completeness}. Read it before ${task}. This is a derived view of the named accepted requirements, PLAN and receipts, not semantic acceptance. Do not edit these generated files or manually duplicate receipt IDs/hashes/results. Record insufficient proof or violated criteria in the ordinary Stage/Work result with evidence references; modify only your assigned source/test/result documents. Review inputs remain read-only. A passed check proves only its named contour and limits, not all business semantics or production readiness.\n</verification_matrix>\n`;
52
+ }
53
+ /** Attachment and packet authority share a short transaction, then both files
54
+ * must be visible before the prompt claims they are ready. */
55
+ export function commitVerificationMatrixStart(context, prepared, attach) {
56
+ context.db.writeTransaction(() => { attach(); acceptVerificationMatrix(context, prepared); });
57
+ publishVerificationMatrix(context, prepared);
58
+ }
59
+ export function resumeVerificationMatrix(context, projectId, runId, stage) {
60
+ const retained = latestRunVerificationMatrix(context, projectId, runId, stage, "input");
61
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
62
+ if (run && JSON.parse(run.index_json).verification_matrix_contract === verificationMatrixContract && !retained)
63
+ throw new AppError("verification_matrix_packet_missing", "Marked Stage has no retained input matrix", 1, { stage, run_id: runId });
64
+ publishVerificationMatrix(context, retained);
65
+ return retained;
66
+ }
67
+ /** Resolve bounded authority outside the owning write transaction. */
68
+ export function prepareRunVerificationMatrix(context, input) {
69
+ const run = context.db.get("SELECT run_root, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
70
+ if (!run)
71
+ throw new AppError("not_found", "Matrix RUN is missing", 1);
72
+ const index = JSON.parse(run.index_json);
73
+ if (index.verification_matrix_contract !== verificationMatrixContract)
74
+ return null;
75
+ const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
76
+ if (!run.run_root)
77
+ throw new AppError("runtime_missing", "Matrix RUN home is missing", 1);
78
+ const home = run.run_root, workspace = run.workspace_root;
79
+ const owning = index.stage_runs?.find(item => item.stage === input.stage);
80
+ const attempt = input.stageAttempt ?? owning?.attempt ?? "try-001";
81
+ const sources = [], packets = [];
82
+ const readSource = (role, root, relative, expected) => {
83
+ const file = safePath(root === "run" ? home : workspace, relative), bytes = fs.readFileSync(file), sha = digest(bytes);
84
+ if (expected && sha !== expected)
85
+ throw new AppError("verification_matrix_source_changed", "Matrix source differs from accepted bytes", 1, { role, path: relative, expected, current: sha });
86
+ sources.push({ role, root, path: slash(relative), sha256: sha });
87
+ return JSON.parse(bytes.toString("utf8"));
88
+ };
89
+ const specifySha = index.stage_runs?.find(item => item.stage === "specify")?.data_sha256;
90
+ const protocolizeSha = index.stage_runs?.find(item => item.stage === "protocolize")?.data_sha256;
91
+ if (!specifySha || !protocolizeSha)
92
+ throw new AppError("verification_matrix_source_unbound", "Marked matrix needs accepted SPECIFY and PROTOCOLIZE source checksums", 1);
93
+ const specify = readSource("specify", "run", "01-specify/specify.json", specifySha);
94
+ const protocolize = readSource("protocolize", "run", "02-protocolize/protocolize-result.json", protocolizeSha);
95
+ const amendments = new Map((protocolize.obligation_amendments ?? []).map(item => [item.target_id, item.replacement]));
96
+ const effective = (items) => items.map(item => ({ ...item, statement: amendments.get(item.id) ?? item.statement }));
97
+ // PROTOCOLIZE data_sha256 binds protocolize-result.json, not its derived
98
+ // Stage report. The report has a separate exact publication receipt.
99
+ const reportReceipts = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_report' ORDER BY id DESC", [input.projectId]);
100
+ const protocolPublication = reportReceipts.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === input.runId && item.stage === "protocolize" && item.attempt === index.stage_runs?.find(stage => stage.stage === "protocolize")?.attempt);
101
+ if (!protocolPublication)
102
+ throw new AppError("verification_matrix_source_unbound", "Marked matrix needs the retained PROTOCOLIZE report receipt", 1);
103
+ const protocolReport = readSource("protocolize_report", "run", "02-protocolize/stage-report.json", protocolPublication?.sha256);
104
+ const protocols = protocolReport.semantic.acceptance;
105
+ const ownership = Object.fromEntries(protocols.map((protocol, i) => [protocol, protocolize.obligation_ownership.filter(item => item.member_keys.includes(protocolize.delivery.members[i].key)).map(item => item.obligation_id)]));
106
+ const skeleton = input.stage === "plan" && input.role === "input";
107
+ const plans = [];
108
+ const predecessor = input.stage === "plan-review" ? "plan" : input.stage === "code" ? "plan-review" : input.stage === "code-review" ? "code" : input.stage === "merge" ? "code-review" : null;
109
+ const priorMatrix = input.role === "input" && predecessor ? latestRunVerificationMatrix(context, input.projectId, input.runId, predecessor) : null;
110
+ if (!skeleton) {
111
+ for (const protocol of protocols) {
112
+ const relative = `.memory-bank/protocol/${protocol}/plan.json`, file = safePath(workspace, relative);
113
+ // PLAN bytes are retained before MERGE cleanup; later recovery does not
114
+ // depend on a feature checkout remaining on disk.
115
+ let bytes;
116
+ if (fs.existsSync(file))
117
+ bytes = fs.readFileSync(file);
118
+ else {
119
+ const retained = latestRunVerificationMatrix(context, input.projectId, input.runId, "merge", "input");
120
+ const prior = retained?.matrix.sources.find(source => source.role === `plan:${protocol}` && source.root === "run");
121
+ if (!prior)
122
+ throw new AppError("verification_matrix_source_missing", "Accepted PLAN was removed without retained source", 1, { protocol_id: protocol });
123
+ bytes = fs.readFileSync(safePath(home, prior.path));
124
+ if (digest(bytes) !== prior.sha256)
125
+ throw new AppError("verification_matrix_source_changed", "Retained PLAN checksum differs", 1, { protocol_id: protocol });
126
+ }
127
+ const plan = JSON.parse(bytes.toString("utf8"));
128
+ const acceptedSource = priorMatrix?.matrix.sources.find(source => source.role === `plan:${protocol}`);
129
+ if (acceptedSource && digest(bytes) !== acceptedSource.sha256)
130
+ throw new AppError("verification_matrix_source_changed", "PLAN differs from accepted predecessor matrix", 1, { protocol_id: protocol, expected: acceptedSource.sha256, current: digest(bytes) });
131
+ if (plan.protocol_id !== protocol || !Array.isArray(plan.checks) || !Array.isArray(plan.items) || !Array.isArray(plan.acceptance))
132
+ throw new AppError("verification_matrix_source_invalid", "Matrix requires a validated structured PLAN", 1, { protocol_id: protocol });
133
+ const sha = digest(bytes), snapshot = `${vnextStageDirectory(input.stage)}/verification/sources/${sha}/plan.json`;
134
+ packets.push(prepareStagePacket(safePath(home, snapshot), bytes));
135
+ sources.push({ role: `plan:${protocol}`, root: "run", path: snapshot, sha256: sha });
136
+ plans.push(plan);
137
+ }
138
+ }
139
+ const semanticFile = input.semanticFile;
140
+ let semanticAssessment;
141
+ if (semanticFile && fs.existsSync(semanticFile)) {
142
+ const relative = path.relative(home, semanticFile);
143
+ readSource("semantic_assessment", "run", relative);
144
+ const source = sources[sources.length - 1];
145
+ semanticAssessment = { root: source.root, path: source.path, sha256: source.sha256 };
146
+ }
147
+ const mergeAcceptanceFile = path.join(home, "07-merge", "merge-gate-acceptance.json");
148
+ const final = input.stage === "merge" && input.role === "output";
149
+ let merge;
150
+ let policyChecks = [];
151
+ if (!skeleton && input.stage !== "merge")
152
+ policyChecks = effectiveCheckDeclarations(workspace, [], ["work", "code", "readiness", "merge", "release"]);
153
+ let selected = input.receipts ?? [];
154
+ if (!input.receipts && priorMatrix && ["code-review", "merge"].includes(input.stage)) {
155
+ const ids = new Set([...priorMatrix.matrix.criteria.flatMap(row => row.checks), ...priorMatrix.matrix.policy_checks].flatMap(fact => typeof fact.result?.id === "string" ? [fact.result.id] : []));
156
+ selected = checkReceipts(context, { projectId: input.projectId, runId: input.runId }).filter(receipt => ids.has(receipt.id));
157
+ }
158
+ let finalBindings;
159
+ if (input.stage === "merge") {
160
+ const gate = readSource("merge_gate", "run", "07-merge/merge-gate.json");
161
+ const semanticRefs = new Set(plans.flatMap(plan => plan.acceptance.flatMap(entry => entry.check_refs.map(id => `${plan.protocol_id}/${id}`))));
162
+ policyChecks = gate.checks.filter(check => !semanticRefs.has(check.canonical_ref ?? check.id));
163
+ if (final) {
164
+ const acceptance = readSource("merge_acceptance", "run", "07-merge/merge-gate-acceptance.json");
165
+ if (acceptance.run_id !== input.runId || acceptance.gate_hash !== gate.gate_hash || acceptance.profile_hash !== gate.profile_hash || !acceptance.accepted_tree)
166
+ throw new AppError("verification_matrix_binding_invalid", "Final matrix has foreign MERGE acceptance", 1);
167
+ finalBindings = new Map();
168
+ for (const binding of acceptance.receipts)
169
+ for (const ref of binding.binding_refs) {
170
+ if (finalBindings.has(ref))
171
+ throw new AppError("verification_matrix_binding_ambiguous", "Final acceptance repeats a check binding", 1, { check_ref: ref });
172
+ finalBindings.set(ref, binding.id);
173
+ }
174
+ const persisted = checkReceipts(context, { projectId: input.projectId, runId: input.runId });
175
+ selected = acceptance.receipts.map(binding => {
176
+ const receipt = selected.find(item => item.id === binding.id && item.input_hash === binding.input_hash);
177
+ if (!receipt)
178
+ throw new AppError("verification_matrix_binding_invalid", "Final matrix lacks bound receipt", 1, { receipt_id: binding.id });
179
+ const original = persisted.find(item => item.id === binding.id && item.input_hash === binding.input_hash);
180
+ if (!original)
181
+ throw new AppError("verification_matrix_binding_invalid", "Final matrix lacks retained native execution receipt", 1, { receipt_id: binding.id });
182
+ return original;
183
+ });
184
+ merge = { merge_request_id: acceptance.merge_request_id, work_id: acceptance.work_id, gate_hash: acceptance.gate_hash, profile_hash: acceptance.profile_hash, accepted_tree: acceptance.accepted_tree, acceptance_ref: { root: "run", path: "07-merge/merge-gate-acceptance.json", sha256: digest(fs.readFileSync(mergeAcceptanceFile)) } };
185
+ }
186
+ }
187
+ const declarations = [...plans.flatMap(plan => plan.checks.map(check => ({ ...check, canonical_ref: `${plan.protocol_id}/${check.id}` }))), ...policyChecks];
188
+ const facts = declarations.map(check => {
189
+ const ref = check.canonical_ref ?? check.id;
190
+ // Unscoped CODE IDs are permitted only by the accepted PSET validator's
191
+ // uniqueness rule. Never silently alias the same short ID across sources.
192
+ const uniqueShortId = plans.filter(plan => plan.checks.some(candidate => candidate.id === check.id)).length === 1;
193
+ const matches = selected.filter(receipt => finalBindings ? finalBindings.get(ref) === receipt.id : receipt.check_refs.includes(ref) || (input.stage !== "merge" && uniqueShortId && receipt.check_refs.includes(check.id)));
194
+ if (matches.length > 1)
195
+ throw new AppError("verification_matrix_binding_ambiguous", "Matrix was supplied multiple current receipts for one declaration", 1, { check_ref: ref });
196
+ const receipt = matches[0];
197
+ const due = input.stage === "merge" ? check.run_at !== "release" : ["work", "code", "readiness"].includes(check.run_at);
198
+ if (!receipt)
199
+ return { id: check.id, canonical_ref: ref, declaration: check, result: null, disposition: skeleton || input.stage.startsWith("plan") ? "not_run" : due && input.role === "output" ? "unavailable" : "not_due" };
200
+ const valid = receipt.status !== "passed" || checkReceiptEvidenceValid(receipt);
201
+ const portable = portableReceipt(receipt, home);
202
+ const receiptFile = safePath(home, portable.receipt_path);
203
+ if (!fs.existsSync(receiptFile))
204
+ return { id: check.id, canonical_ref: ref, declaration: check, result: portable, disposition: "unavailable", reason: "receipt_file_missing" };
205
+ const bytes = fs.readFileSync(receiptFile);
206
+ sources.push({ role: `receipt:${ref}`, root: "run", path: portable.receipt_path, sha256: digest(bytes) });
207
+ return { id: check.id, canonical_ref: ref, declaration: check, result: portable, disposition: valid ? "retained" : "unavailable", ...(!valid ? { reason: "receipt_evidence_invalid" } : {}) };
208
+ });
209
+ const works = context.db.all("SELECT work_id, payload_json FROM works WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]).flatMap(work => {
210
+ const source = work.payload_json ? JSON.parse(work.payload_json).source : null;
211
+ return source ? [{ work_id: work.work_id, protocol_id: source.protocol_id, plan_item_id: source.plan_item_id }] : [];
212
+ });
213
+ const repair = index.variables?.["merge.source_repair"];
214
+ const header = { project_id: input.projectId, run_id: input.runId, stage: input.stage, stage_attempt: attempt, role: input.role, completeness: input.completeness ?? (skeleton ? "skeleton" : input.stage.startsWith("plan") ? "declared" : final ? "final" : "execution"), ...(repair?.cycle ? { review_cycle: repair.cycle } : {}), ...(merge ? { merge } : {}) };
215
+ const matrix = buildVerificationMatrix({ header, sources, requirements: effective(specify.requirements), criteria: effective(specify.acceptance_criteria), ownership, plans, policyChecks, facts, works, ...(semanticAssessment ? { semanticAssessment } : {}) });
216
+ const root = `${vnextStageDirectory(input.stage)}/verification/${matrix.header.source_fingerprint}`;
217
+ const json = prepareStagePacket(safePath(home, `${root}/verification-matrix.json`), Buffer.from(stableJson(matrix)));
218
+ const markdown = prepareStagePacket(safePath(home, `${root}/verification-matrix.md`), Buffer.from(renderVerificationMatrix(matrix)));
219
+ packets.push(json, markdown);
220
+ const metadata = { contract: verificationMatrixContract, role: header.role, completeness: header.completeness, source_fingerprint: matrix.header.source_fingerprint, json: { path: slash(path.relative(home, json.file)), sha256: json.sha256 }, markdown: { path: slash(path.relative(home, markdown.file)), sha256: markdown.sha256 } };
221
+ const predecessors = (index.stage_runs ?? []).filter(item => item.stage !== input.stage && item.status === "done").map(item => ({ stage: item.stage, ...(item.attempt ? { attempt: item.attempt } : {}), ...(item.data_sha256 ? { data_sha256: item.data_sha256 } : {}) }));
222
+ return { projectId: input.projectId, runId: input.runId, stage: input.stage, home, attempt, generation, packets, matrix, metadata, predecessors };
223
+ }
224
+ /** Called inside the existing owning settlement; publication is after COMMIT. */
225
+ export function acceptVerificationMatrix(context, prepared) {
226
+ if (!prepared)
227
+ return;
228
+ assertRunMutationAllowed(context, prepared.projectId, prepared.runId, prepared.generation);
229
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [prepared.projectId, prepared.runId]);
230
+ const index = JSON.parse(run.index_json);
231
+ if (index.stage_runs?.find(item => item.stage === prepared.stage)?.attempt !== prepared.attempt)
232
+ throw new AppError("verification_matrix_owner_changed", "Matrix owning attempt changed during preparation", 1, { stage: prepared.stage, expected_attempt: prepared.attempt });
233
+ for (const source of prepared.predecessors ?? []) {
234
+ const current = index.stage_runs?.find(item => item.stage === source.stage);
235
+ if (current?.attempt !== source.attempt || current?.data_sha256 !== source.data_sha256)
236
+ throw new AppError("verification_matrix_source_changed", "Accepted predecessor changed after matrix preparation", 1, { stage: source.stage });
237
+ }
238
+ for (const packet of prepared.packets)
239
+ acceptStagePacket(context, { projectId: prepared.projectId, runId: prepared.runId, stage: prepared.stage, prepared: packet });
240
+ }
241
+ export function publishVerificationMatrix(context, prepared) {
242
+ if (!prepared)
243
+ return;
244
+ for (const packet of prepared.packets) {
245
+ publishStagePacket(context, prepared.projectId, prepared.runId, prepared.stage, packet.file);
246
+ if (!fs.existsSync(packet.file) || digest(fs.readFileSync(packet.file)) !== packet.sha256)
247
+ throw new AppError("publication_pending", "Matrix packet is not published with its accepted checksum", 1, { file: packet.file, effect: "committed" });
248
+ }
249
+ }
250
+ export function latestRunVerificationMatrix(context, projectId, runId, stage, role) {
251
+ const run = context.db.get("SELECT run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
252
+ if (!run?.run_root)
253
+ return null;
254
+ const index = JSON.parse(run.index_json);
255
+ const attempt = index.stage_runs?.find(item => item.stage === stage)?.attempt;
256
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_packet' ORDER BY id DESC", [projectId]);
257
+ for (const row of rows) {
258
+ const packet = JSON.parse(row.payload_json);
259
+ if (packet.run_id !== runId || packet.stage !== stage || packet.attempt !== attempt || !packet.file.endsWith("/verification-matrix.json"))
260
+ continue;
261
+ const bytes = Buffer.from(packet.base64, "base64");
262
+ if (digest(bytes) !== packet.sha256)
263
+ throw new AppError("stage_packet_invalid", "Retained matrix packet is corrupt", 1);
264
+ const matrix = JSON.parse(bytes.toString("utf8"));
265
+ if (role && matrix.header.role !== role)
266
+ continue;
267
+ if (matrix.schema_id !== verificationMatrixContract || matrix.header.run_id !== runId || matrix.header.stage !== stage || matrix.header.stage_attempt !== attempt)
268
+ throw new AppError("stage_packet_invalid", "Retained matrix owner differs from its packet", 1);
269
+ safePath(run.run_root, path.relative(run.run_root, packet.file), false);
270
+ const mdFile = packet.file.replace(/\.json$/, ".md");
271
+ const paired = rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.stage === stage && item.attempt === attempt && item.file === mdFile);
272
+ if (!paired)
273
+ throw new AppError("stage_packet_invalid", "Retained matrix lacks its exact Markdown packet", 1);
274
+ const mdBytes = Buffer.from(paired.base64, "base64");
275
+ if (digest(mdBytes) !== paired.sha256)
276
+ throw new AppError("stage_packet_invalid", "Retained matrix Markdown checksum differs", 1);
277
+ const md = { file: mdFile, bytes: mdBytes, sha256: paired.sha256 };
278
+ const json = { file: packet.file, bytes, sha256: packet.sha256 };
279
+ const metadata = { contract: verificationMatrixContract, role: matrix.header.role, completeness: matrix.header.completeness, source_fingerprint: matrix.header.source_fingerprint, json: { path: slash(path.relative(run.run_root, packet.file)), sha256: packet.sha256 }, markdown: { path: slash(path.relative(run.run_root, mdFile)), sha256: md.sha256 } };
280
+ const sourcePackets = matrix.sources.filter(source => source.role.startsWith("plan:") && source.root === "run").map(source => {
281
+ const file = safePath(run.run_root, source.path, false);
282
+ const retained = rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.stage === stage && item.attempt === attempt && item.file === file && item.sha256 === source.sha256);
283
+ if (!retained)
284
+ throw new AppError("stage_packet_invalid", "Matrix PLAN source lacks retained bytes", 1, { path: source.path });
285
+ const bytes = Buffer.from(retained.base64, "base64");
286
+ if (digest(bytes) !== source.sha256)
287
+ throw new AppError("stage_packet_invalid", "Matrix PLAN source checksum differs", 1, { path: source.path });
288
+ return { file, bytes, sha256: source.sha256 };
289
+ });
290
+ const prepared = { projectId, runId, stage, home: run.run_root, attempt: packet.attempt, packets: [...sourcePackets, json, md], matrix, metadata };
291
+ return prepared;
292
+ }
293
+ return null;
294
+ }
295
+ function portableReceipt(receipt, home) {
296
+ const relative = (file) => slash(path.relative(home, safePath(home, path.relative(home, file))));
297
+ return { ...receipt, receipt_path: relative(receipt.receipt_path), completion_path: relative(receipt.completion_path), stdout_path: relative(receipt.stdout_path), stderr_path: relative(receipt.stderr_path), artifacts: receipt.artifacts.map(item => ({ ...item, path: relative(item.path) })) };
298
+ }
299
+ function safePath(root, relative, inspectFilesystem = true) {
300
+ if (path.isAbsolute(relative) || relative.split(/[\\/]/).includes(".."))
301
+ throw new AppError("verification_matrix_path_invalid", "Matrix reference must remain inside its declared root", 1, { path: relative });
302
+ const resolved = path.resolve(root, relative);
303
+ // Retained packet resolution is SQL-only and can run in a settlement writer.
304
+ // Publication and all new filesystem inputs enforce realpath containment.
305
+ if (!inspectFilesystem)
306
+ return resolved;
307
+ let ancestor = resolved;
308
+ while (!fs.existsSync(ancestor) && path.dirname(ancestor) !== ancestor)
309
+ ancestor = path.dirname(ancestor);
310
+ if (fs.existsSync(ancestor)) {
311
+ const actual = fs.realpathSync(ancestor), realRoot = fs.realpathSync(root);
312
+ if (actual !== realRoot && !actual.startsWith(`${realRoot}${path.sep}`))
313
+ throw new AppError("verification_matrix_path_invalid", "Matrix source escapes its root through a symlink", 1, { path: relative });
314
+ }
315
+ return resolved;
316
+ }
317
+ function digest(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
318
+ function compare(a, b) { return a < b ? -1 : a > b ? 1 : 0; }
319
+ function unique(items) { return [...new Set(items)].sort(compare); }
320
+ function slash(value) { return value.split(path.sep).join("/"); }
321
+ function escape(value) { return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\|/g, "&#124;").replace(/[\r\n]+/g, "<br>").replace(/`/g, "&#96;"); }
322
+ function canonical(value) { if (Array.isArray(value))
323
+ return value.map(canonical); if (value && typeof value === "object")
324
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => compare(a, b)).map(([key, item]) => [key, canonical(item)])); return value; }
325
+ function stableJson(value) { return `${JSON.stringify(canonical(value), null, 2)}\n`; }
@@ -17,6 +17,7 @@ import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, finishStageCo
17
17
  import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
18
18
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
19
19
  import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
20
+ import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix, publishVerificationMatrix } from "./verification-matrix.js";
20
21
  import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
21
22
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
22
23
  import { stageGraphScope } from "./stage-work-graph.js";
@@ -50,7 +51,7 @@ export function prepareVnextCodeReviewStart(context, input) {
50
51
  const common = { projectRoot, project, run, home, root, rootWork, existingPrompt };
51
52
  if (prior && fs.existsSync(existingPrompt))
52
53
  return { ...common, resumed: true, prior, prompt: fs.readFileSync(existingPrompt, "utf8"), orchestration: readFanoutDescriptor(root) };
53
- prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
54
+ const attachment = prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
54
55
  const mode = effectiveMode(home, run);
55
56
  const cycle = reviewCycle(run);
56
57
  const codeReportSha256 = sha256File(path.join(home, "05-code", "stage-report.json"));
@@ -61,7 +62,8 @@ export function prepareVnextCodeReviewStart(context, input) {
61
62
  const checks = mode.mode === "off" ? [] : acceptedCodeChecks(context, project.id, run.id).map(({ id, purpose }) => ({ id, purpose }));
62
63
  for (const name of ["review-context.json", ...(mode.mode === "off" ? ["decision.json"] : ["review-work-batch.json", "fanout.json"])])
63
64
  prepareOutputFile(path.join(root, name));
64
- return { ...common, resumed: false, mode, cycle, codeReportSha256, groups, batch, items, template, checks };
65
+ const matrix = mode.mode === "off" ? null : prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "input", stageAttempt: attachment.attempt });
66
+ return { ...common, resumed: false, mode, cycle, codeReportSha256, groups, batch, items, template, checks, matrix };
65
67
  }
66
68
  export function startVnextCodeReview(context, input, prepared = prepareVnextCodeReviewStart(context, input)) {
67
69
  const { projectRoot, project, run, root, rootWork, existingPrompt } = prepared;
@@ -69,6 +71,8 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
69
71
  assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
70
72
  fs.mkdirSync(root, { recursive: true });
71
73
  if (prepared.resumed) {
74
+ if (effectiveMode(prepared.home, run).mode !== "off")
75
+ resumeVerificationMatrix(context, project.id, run.id, stage);
72
76
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
73
77
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath: existingPrompt, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
74
78
  const prompt = fs.readFileSync(existingPrompt, "utf8");
@@ -76,6 +80,7 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
76
80
  return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.prior, id: binding.work_session_id, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
77
81
  }
78
82
  const { mode, cycle, groups, batch, items, template, checks, codeReportSha256 } = prepared;
83
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" }, true);
79
84
  const aspectMapChecksums = mode.mode === "off" ? [] : fs.readdirSync(path.join(run.run_root, "03-plan"), { withFileTypes: true })
80
85
  .filter(entry => entry.isDirectory() && fs.existsSync(path.join(run.run_root, "03-plan", entry.name, "aspect-map.json")))
81
86
  .map(entry => ({ path: `03-plan/${entry.name}/aspect-map.json`, sha256: sha256File(path.join(run.run_root, "03-plan", entry.name, "aspect-map.json")) }))
@@ -90,18 +95,21 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
90
95
  fs.writeFileSync(promptPath, prompt);
91
96
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
92
97
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
93
- attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
98
+ const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" };
99
+ attachFlowRunStage(context, attachmentInput, prepareFlowRunStageAttachment(context, attachmentInput));
94
100
  return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
95
101
  }
96
102
  const batchFile = path.join(root, "review-work-batch.json");
97
103
  fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
98
- const prompt = `${orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups, template, checks })}\n${stageLifecycleInstruction}\n`;
104
+ const prompt = `${orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups, template, checks })}\n${stageLifecycleInstruction}\n${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, prepared.home, "reviewing whether current evidence proves the accepted obligations")}\n`;
99
105
  fs.writeFileSync(promptPath, prompt);
100
106
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
101
107
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
102
108
  const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch, prepared: items, stage, cycle });
103
109
  const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
104
- attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1", inputSha256: sha256Value(JSON.stringify({ batch: sha256File(batchFile), aspect_maps: aspectMapChecksums })) });
110
+ const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1", inputSha256: sha256Value(JSON.stringify({ batch: sha256File(batchFile), aspect_maps: aspectMapChecksums })) };
111
+ const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
112
+ commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
105
113
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
106
114
  return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
107
115
  }
@@ -208,7 +216,8 @@ export async function finishVnextCodeReview(context, input) {
208
216
  fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision }, null, 2)}\n`);
209
217
  fs.renameSync(temporary, contextFile);
210
218
  const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, checkRefsByFinding, decisionReasonsByFinding: Object.fromEntries(canonical.decision.findings.map((item) => [item.finding_ref, item.reason])), objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
211
- return { ok: true, run_id: run.id, stage, outcome: "repair_required", repair, instruction: "The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. When it completes, invoke next.finish_command; this review cycle uses the decision already accepted by the runtime.", next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
219
+ const retained = latestRunVerificationMatrix(context, project.id, run.id, stage, "input");
220
+ return { ok: true, run_id: run.id, stage, outcome: "repair_required", repair, ...(retained ? { verification_matrix: retained.metadata } : {}), instruction: `The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. When it completes, invoke next.finish_command; this review cycle uses the decision already accepted by the runtime.\n${verificationMatrixInstruction(retained?.metadata ?? null, home, "the accepted review decision and repair")}`, next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
212
221
  }
213
222
  repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
214
223
  const incompleteRepairs = repairs.filter((work) => work.status !== "completed");
@@ -224,16 +233,21 @@ export async function finishVnextCodeReview(context, input) {
224
233
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
225
234
  }
226
235
  const receipts = await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, artifactDir: stageDir, scope: "aggregate", checks: finalChecks, ...(input.retryCheckId ? { retryReceiptId: input.retryCheckId, retryReason: input.retryReason } : {}), ...(input.progress ? { progress: input.progress } : {}) });
236
+ const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", receipts, semanticFile: decisionFile });
227
237
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
228
- if (failed.length)
238
+ if (failed.length) {
239
+ context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
240
+ publishVerificationMatrix(context, matrix);
229
241
  throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
230
242
  failures: failed,
231
243
  outcome: "repair_required",
232
244
  retry_after_workspace_change: true,
233
245
  workspace_fingerprint: failed[0].workspace_fingerprint,
234
246
  continuation: registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks: finalChecks, generation }),
235
- instruction: "Repair Work is already registered. End this Turn so the controller can launch a fresh child; after it finishes, repeat finish with the already accepted review decision. Do not redo review."
247
+ ...(matrix ? { verification_matrix: matrix.metadata } : {}),
248
+ instruction: `Repair Work is already registered. End this Turn so the controller can launch a fresh child; after it finishes, repeat finish with the already accepted review decision. Do not redo review.\n${verificationMatrixInstruction(matrix?.metadata ?? null, home, "the current aggregate failures and repair")}`
236
249
  });
250
+ }
237
251
  const stopTarget = executionStopTarget(run);
238
252
  const nextAction = stopTarget === "merge_completed" ? "start_merge" : "code_review_completed";
239
253
  const now = context.now();
@@ -245,6 +259,8 @@ export async function finishVnextCodeReview(context, input) {
245
259
  let mergeRequest = null;
246
260
  assertRunMutationAllowed(context, project.id, run.id, generation);
247
261
  const prepareMerge = nextAction === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
262
+ if (matrix)
263
+ Object.assign(report.semantic, { verification_matrix: matrix.metadata });
248
264
  const preparedReport = prepareStageReport(report);
249
265
  const completionInput = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: report.mechanical.git };
250
266
  withStageSettlement(context, () => {
@@ -256,6 +272,7 @@ export async function finishVnextCodeReview(context, input) {
256
272
  }
257
273
  mergeRequest = prepareMerge?.() ?? null;
258
274
  const preparedCompletion = prepareFlowRunStageCompletion(context, completionInput);
275
+ acceptVerificationMatrix(context, matrix);
259
276
  acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
260
277
  completeFlowRunStage(context, completionInput, preparedCompletion);
261
278
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });