@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.
- package/CHANGELOG.md +9 -0
- package/dist/build-info.json +5 -5
- package/dist/cli/hook-ingress.js +4 -3
- package/dist/cli/native-hook-ingress.js +34 -2
- package/dist/harness-runtime/bin/dd-agy.mjs +6 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +16 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +12 -2
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +60 -8
- package/dist/harness-runtime/lib/dd-zcode.mjs +28 -13
- package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
- package/dist/harness-runtime/lib/lifecycle-command.mjs +218 -0
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +36 -5
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/services/code-checks.js +40 -0
- package/dist/services/codex-hook-delivery.js +6 -2
- package/dist/services/hooks.js +51 -59
- package/dist/services/lifecycle-command.js +1 -383
- package/dist/services/lifecycle-contract.js +1 -30
- package/dist/services/lifecycle-invocations.js +34 -13
- package/dist/services/managed-processes.js +42 -6
- package/dist/services/run-control.js +7 -1
- package/dist/services/runs.js +60 -47
- package/dist/services/runtime-scope-stop.js +23 -2
- package/dist/services/stage-report-renderer.js +19 -0
- package/dist/services/verification-matrix.js +325 -0
- package/dist/services/vnext-code-review.js +25 -8
- package/dist/services/vnext-code.js +38 -11
- package/dist/services/vnext-merge.js +30 -7
- package/dist/services/vnext-plan-review.js +22 -6
- package/dist/services/vnext-plan.js +45 -17
- package/dist/services/work-registry.js +13 -1
- package/dist/storage/database.js +43 -9
- 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\|/g, "|").replace(/[\r\n]+/g, "<br>").replace(/`/g, "`"); }
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 });
|