@mstar-harness/engine 3.10.2 → 3.11.0
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/dist/audit.d.ts +52 -1
- package/dist/audit.js +269 -13
- package/dist/coordination-write.d.ts +5 -3
- package/dist/coordination.d.ts +5 -0
- package/dist/engine.js +764 -37
- package/dist/index.d.ts +4 -4
- package/dist/workflow.d.ts +80 -1
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -623,7 +623,7 @@ function validatePlanProgress(value, what = "coordination.progress") {
|
|
|
623
623
|
}
|
|
624
624
|
return violations;
|
|
625
625
|
}
|
|
626
|
-
function validatePlanHandoff(value, what = "coordination.handoff") {
|
|
626
|
+
function validatePlanHandoff(value, what = "coordination.handoff", route = "integration") {
|
|
627
627
|
if (!isPlainObject2(value))
|
|
628
628
|
return [invalid("coordination.row.handoff-shape", `${what} must be an object`)];
|
|
629
629
|
const allowed = [
|
|
@@ -763,7 +763,25 @@ function validatePlanHandoff(value, what = "coordination.handoff") {
|
|
|
763
763
|
}
|
|
764
764
|
}
|
|
765
765
|
if ((value.state === "integrating" || value.state === "merged" || value.state === "completed") && value.integration === undefined) {
|
|
766
|
-
|
|
766
|
+
if (route === "standalone-development" && value.state === "completed") {
|
|
767
|
+
if (value.completed_at === undefined) {
|
|
768
|
+
violations.push(invalid("coordination.row.handoff-field", `${what}.state completed requires completed_at for a standalone handoff`));
|
|
769
|
+
}
|
|
770
|
+
if (!isNonEmptyString(value.accepted_at)) {
|
|
771
|
+
violations.push(invalid("coordination.row.handoff-field", `${what}.accepted_at is required for a standalone completed handoff`));
|
|
772
|
+
}
|
|
773
|
+
if (!isNonEmptyString(value.accepted_by)) {
|
|
774
|
+
violations.push(invalid("coordination.row.handoff-field", `${what}.accepted_by is required for a standalone completed handoff`));
|
|
775
|
+
}
|
|
776
|
+
if (value.qc === undefined) {
|
|
777
|
+
violations.push(invalid("coordination.row.handoff-field", `${what}.qc is required for a standalone completed handoff`));
|
|
778
|
+
}
|
|
779
|
+
if (value.qa === undefined) {
|
|
780
|
+
violations.push(invalid("coordination.row.handoff-field", `${what}.qa is required for a standalone completed handoff`));
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
violations.push(invalid("coordination.row.handoff-field", `${what}.state ${String(value.state)} requires integration`));
|
|
784
|
+
}
|
|
767
785
|
}
|
|
768
786
|
return violations;
|
|
769
787
|
}
|
|
@@ -799,7 +817,7 @@ function validatePreparedCoordination(value, what = "coordination.prepared") {
|
|
|
799
817
|
}
|
|
800
818
|
return violations;
|
|
801
819
|
}
|
|
802
|
-
function validateRowCoordination(value, what = "coordination") {
|
|
820
|
+
function validateRowCoordination(value, what = "coordination", route = "integration") {
|
|
803
821
|
if (!isPlainObject2(value))
|
|
804
822
|
return [invalid("coordination.row.shape", `${what} must be an object`)];
|
|
805
823
|
const allowed = ["revision", "prepared", "session", "progress", "handoff"];
|
|
@@ -818,7 +836,7 @@ function validateRowCoordination(value, what = "coordination") {
|
|
|
818
836
|
if (value.progress !== undefined)
|
|
819
837
|
violations.push(...validatePlanProgress(value.progress, `${what}.progress`));
|
|
820
838
|
if (value.handoff !== undefined)
|
|
821
|
-
violations.push(...validatePlanHandoff(value.handoff, `${what}.handoff
|
|
839
|
+
violations.push(...validatePlanHandoff(value.handoff, `${what}.handoff`, route));
|
|
822
840
|
if (value.handoff !== undefined && value.session === undefined) {
|
|
823
841
|
violations.push(invalid("coordination.row.handoff-field", `${what}.handoff requires a bound plan session`));
|
|
824
842
|
}
|
|
@@ -1331,6 +1349,45 @@ var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
|
|
|
1331
1349
|
var WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
|
|
1332
1350
|
var WORKFLOW_DELIVERY_KINDS = ["development", "verification/report-only"];
|
|
1333
1351
|
var WORKFLOW_COMPOUND_OUTCOMES = ["created", "updated", "skipped"];
|
|
1352
|
+
function isStandaloneDevelopmentWorkflow(snapshot) {
|
|
1353
|
+
return snapshot.type === "plan" && snapshot.delivery_kind === "development" && Array.isArray(snapshot.plans) && snapshot.plans.length === 1;
|
|
1354
|
+
}
|
|
1355
|
+
function rowValidationRoute(snapshot, row) {
|
|
1356
|
+
if (isStandaloneDevelopmentWorkflow(snapshot) && snapshot.plans[0]?.id === row.id) {
|
|
1357
|
+
return "standalone-development";
|
|
1358
|
+
}
|
|
1359
|
+
return "integration";
|
|
1360
|
+
}
|
|
1361
|
+
function validateStandaloneCompletedCoherence(snapshot, row) {
|
|
1362
|
+
const violations = [];
|
|
1363
|
+
if (!isStandaloneDevelopmentWorkflow(snapshot) || row.id !== snapshot.plans[0]?.id)
|
|
1364
|
+
return violations;
|
|
1365
|
+
const coordination = row.coordination;
|
|
1366
|
+
if (!isPlainObject2(coordination) || !isPlainObject2(coordination.handoff))
|
|
1367
|
+
return violations;
|
|
1368
|
+
const handoff = coordination.handoff;
|
|
1369
|
+
if (handoff.state !== "completed" || handoff.integration !== undefined)
|
|
1370
|
+
return violations;
|
|
1371
|
+
if (row.status !== "Done") {
|
|
1372
|
+
violations.push(violation3("high", "coordination.row.handoff-field", `standalone completed handoff requires row ${String(row.id)} to be Done`));
|
|
1373
|
+
}
|
|
1374
|
+
if (row.execution_lease !== undefined) {
|
|
1375
|
+
violations.push(violation3("high", "coordination.row.handoff-field", `standalone completed handoff requires no execution lease on row ${String(row.id)}`));
|
|
1376
|
+
}
|
|
1377
|
+
if (snapshot.integration_merge_lease !== undefined) {
|
|
1378
|
+
violations.push(violation3("high", "coordination.row.handoff-field", "standalone completed handoff requires no integration_merge_lease on the snapshot"));
|
|
1379
|
+
}
|
|
1380
|
+
const source = snapshot.branch?.source;
|
|
1381
|
+
const target = snapshot.branch?.target;
|
|
1382
|
+
if (!isNonEmptyString(source) || !isNonEmptyString(target)) {
|
|
1383
|
+
violations.push(violation3("high", "coordination.row.handoff-field", "standalone completed handoff requires nonblank branch.source and branch.target"));
|
|
1384
|
+
} else {
|
|
1385
|
+
if (handoff.source_branch !== source) {
|
|
1386
|
+
violations.push(violation3("high", "coordination.row.handoff-field", `standalone completed handoff source_branch ${String(handoff.source_branch)} must equal branch.source ${source}`));
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return violations;
|
|
1390
|
+
}
|
|
1334
1391
|
function stableJson(value) {
|
|
1335
1392
|
if (Array.isArray(value))
|
|
1336
1393
|
return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -1450,13 +1507,17 @@ function validateWorkflowSnapshot(doc) {
|
|
|
1450
1507
|
} else if (!Array.isArray(doc.plans)) {
|
|
1451
1508
|
violations.push(violation3("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
|
|
1452
1509
|
} else {
|
|
1510
|
+
const snapshotDoc = doc;
|
|
1453
1511
|
for (const row of doc.plans) {
|
|
1454
1512
|
violations.push(...validatePlanRow(row).violations);
|
|
1455
1513
|
if (isPlainObject2(row) && row.execution_lease !== undefined) {
|
|
1456
1514
|
violations.push(...validateExecutionLease(row.execution_lease).violations);
|
|
1457
1515
|
}
|
|
1458
1516
|
if (isPlainObject2(row) && row.coordination !== undefined) {
|
|
1459
|
-
|
|
1517
|
+
const planRow = row;
|
|
1518
|
+
const route = rowValidationRoute(snapshotDoc, planRow);
|
|
1519
|
+
violations.push(...validateRowCoordination(row.coordination, `plans[${String(row.id)}].coordination`, route));
|
|
1520
|
+
violations.push(...validateStandaloneCompletedCoherence(snapshotDoc, planRow));
|
|
1460
1521
|
}
|
|
1461
1522
|
}
|
|
1462
1523
|
}
|
|
@@ -2023,6 +2084,158 @@ async function registerPlanWorkflow(workflowId, options) {
|
|
|
2023
2084
|
return { workflowId, snapshotPath, recovered: false };
|
|
2024
2085
|
});
|
|
2025
2086
|
}
|
|
2087
|
+
function iterationWorkflowRegistrationIdentity(snapshot) {
|
|
2088
|
+
const coordinator = snapshot.coordination?.coordinator;
|
|
2089
|
+
return stableJson({
|
|
2090
|
+
id: snapshot.id,
|
|
2091
|
+
type: snapshot.type,
|
|
2092
|
+
status: snapshot.status,
|
|
2093
|
+
compass_ref: snapshot.compass_ref ?? null,
|
|
2094
|
+
branch: snapshot.branch ?? null,
|
|
2095
|
+
project: snapshot.project ?? null,
|
|
2096
|
+
plans: snapshot.plans,
|
|
2097
|
+
coordinator: coordinator ? { session_id: coordinator.session_id, session_file: coordinator.session_file } : null
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
async function registerIterationWorkflow(workflowId, options) {
|
|
2101
|
+
const refuse = (detail) => new Error(`registerIterationWorkflow: ${detail}`);
|
|
2102
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
2103
|
+
throw refuse("options must be an object (harnessDir, compassRef, branch, rows)");
|
|
2104
|
+
}
|
|
2105
|
+
if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
|
|
2106
|
+
throw refuse("options.harnessDir is required (must contain status.json + workflows/)");
|
|
2107
|
+
}
|
|
2108
|
+
assertSafePathComponent(workflowId, "workflow id");
|
|
2109
|
+
if (typeof options.compassRef !== "string" || options.compassRef.trim() === "") {
|
|
2110
|
+
throw refuse("options.compassRef must be a non-empty string");
|
|
2111
|
+
}
|
|
2112
|
+
if (typeof options.branch !== "object" || options.branch === null || Array.isArray(options.branch)) {
|
|
2113
|
+
throw refuse("options.branch must be an object (base, integration, target)");
|
|
2114
|
+
}
|
|
2115
|
+
for (const anchor of ["base", "integration", "target"]) {
|
|
2116
|
+
const value = options.branch[anchor];
|
|
2117
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2118
|
+
throw refuse(`options.branch.${anchor} must be a non-empty string`);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
if (options.project !== undefined && (typeof options.project !== "string" || options.project.trim() === "")) {
|
|
2122
|
+
throw refuse("options.project must be a non-empty string when given");
|
|
2123
|
+
}
|
|
2124
|
+
const rows = options.rows;
|
|
2125
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
2126
|
+
throw refuse("options.rows must be a non-empty array of { id, title, file }");
|
|
2127
|
+
}
|
|
2128
|
+
const seenRowIds = new Set;
|
|
2129
|
+
for (const row of rows) {
|
|
2130
|
+
if (typeof row !== "object" || row === null || Array.isArray(row)) {
|
|
2131
|
+
throw refuse("each row must be an object ({ id, title, file })");
|
|
2132
|
+
}
|
|
2133
|
+
for (const field of ["id", "title", "file"]) {
|
|
2134
|
+
if (typeof row[field] !== "string" || row[field].trim() === "") {
|
|
2135
|
+
throw refuse(`each row.${field} must be a non-empty string`);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
if ("status" in row) {
|
|
2139
|
+
throw refuse(`row ${JSON.stringify(row.id)} supplies a status — rows are always written status "Todo"; ` + "a state transition is requested through the lifecycle seams, never at registration");
|
|
2140
|
+
}
|
|
2141
|
+
if (seenRowIds.has(row.id)) {
|
|
2142
|
+
throw refuse(`duplicate row id ${JSON.stringify(row.id)} — plan row ids are unique per workflow`);
|
|
2143
|
+
}
|
|
2144
|
+
seenRowIds.add(row.id);
|
|
2145
|
+
}
|
|
2146
|
+
const startedAt = options.startedAt ?? new Date().toISOString();
|
|
2147
|
+
if (!isCloseTimestamp(startedAt)) {
|
|
2148
|
+
throw refuse("options.startedAt must be a valid YYYY-MM-DD date or RFC3339 timestamp");
|
|
2149
|
+
}
|
|
2150
|
+
const harnessDir = resolve6(options.harnessDir);
|
|
2151
|
+
const statusPath = join6(harnessDir, "status.json");
|
|
2152
|
+
const workflowDir = join6(harnessDir, "workflows", workflowId);
|
|
2153
|
+
const snapshotPath = join6(workflowDir, WORKFLOW_SNAPSHOT_FILE);
|
|
2154
|
+
const store = getArtifactStore();
|
|
2155
|
+
const snapshot = {
|
|
2156
|
+
schema_version: 1,
|
|
2157
|
+
id: workflowId,
|
|
2158
|
+
type: "iteration",
|
|
2159
|
+
status: "running",
|
|
2160
|
+
started_at: startedAt,
|
|
2161
|
+
updated_at: startedAt.slice(0, 10),
|
|
2162
|
+
compass_ref: options.compassRef,
|
|
2163
|
+
branch: { base: options.branch.base, integration: options.branch.integration, target: options.branch.target },
|
|
2164
|
+
plans: rows.map((r) => ({
|
|
2165
|
+
id: r.id,
|
|
2166
|
+
title: r.title,
|
|
2167
|
+
file: r.file,
|
|
2168
|
+
status: "Todo",
|
|
2169
|
+
metadata: {
|
|
2170
|
+
iteration_refs: [options.compassRef],
|
|
2171
|
+
spec_integration_branch: options.branch.integration,
|
|
2172
|
+
merge_target: options.branch.integration
|
|
2173
|
+
}
|
|
2174
|
+
}))
|
|
2175
|
+
};
|
|
2176
|
+
if (options.project !== undefined)
|
|
2177
|
+
snapshot.project = options.project;
|
|
2178
|
+
const entry = {
|
|
2179
|
+
id: workflowId,
|
|
2180
|
+
type: "iteration",
|
|
2181
|
+
started_at: snapshot.started_at,
|
|
2182
|
+
dir: `workflows/${workflowId}`
|
|
2183
|
+
};
|
|
2184
|
+
const entryGate = validateWorkflowEntry(entry);
|
|
2185
|
+
if (!entryGate.ok) {
|
|
2186
|
+
throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
|
|
2187
|
+
}
|
|
2188
|
+
return withStatusWriteLock(statusPath, async () => {
|
|
2189
|
+
const rootDoc = readJson(statusPath);
|
|
2190
|
+
const workflows = Array.isArray(rootDoc.workflows) ? rootDoc.workflows : [];
|
|
2191
|
+
if (workflows.some((candidate) => isPlainObject2(candidate) && candidate.id === workflowId)) {
|
|
2192
|
+
throw new Error(`refusing to register workflow ${JSON.stringify(workflowId)}: it is already registered ` + `(root entry in ${statusPath}) — registration is create-only; ` + "remove that workflow before registering again");
|
|
2193
|
+
}
|
|
2194
|
+
if (existsSync4(snapshotPath)) {
|
|
2195
|
+
const existing = readWorkflowSnapshot(workflowDir);
|
|
2196
|
+
if (existing.snapshot.id !== workflowId || iterationWorkflowRegistrationIdentity(existing.snapshot) !== iterationWorkflowRegistrationIdentity(snapshot)) {
|
|
2197
|
+
throw new Error(`refusing to register workflow ${JSON.stringify(workflowId)}: snapshot ${snapshotPath} already exists ` + "with a different registration identity — remove that workflow or register under a different id");
|
|
2198
|
+
}
|
|
2199
|
+
const recoveryEntry = {
|
|
2200
|
+
id: workflowId,
|
|
2201
|
+
type: "iteration",
|
|
2202
|
+
started_at: existing.snapshot.started_at,
|
|
2203
|
+
dir: `workflows/${workflowId}`
|
|
2204
|
+
};
|
|
2205
|
+
const recoveryGate = validateWorkflowEntry(recoveryEntry);
|
|
2206
|
+
if (!recoveryGate.ok) {
|
|
2207
|
+
throw new Error(`refusing to register invalid workflow entry: ${recoveryGate.violations.map((v) => v.message).join("; ")}`);
|
|
2208
|
+
}
|
|
2209
|
+
await registerWorkflowEntryLocked(statusPath, recoveryEntry);
|
|
2210
|
+
return { workflowId, snapshotPath, recovered: true };
|
|
2211
|
+
}
|
|
2212
|
+
let createdVersion;
|
|
2213
|
+
try {
|
|
2214
|
+
await writeWorkflowSnapshot(snapshot, workflowDir, { createOnly: true });
|
|
2215
|
+
createdVersion = readArtifactBytes(snapshotPath)?.version;
|
|
2216
|
+
await registerWorkflowEntryLocked(statusPath, entry);
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
if (createdVersion !== undefined) {
|
|
2219
|
+
await withStatusWriteLock(snapshotPath, async () => {
|
|
2220
|
+
const current = readArtifactBytes(snapshotPath);
|
|
2221
|
+
if (current === undefined || current.version !== createdVersion)
|
|
2222
|
+
return;
|
|
2223
|
+
const remove = store.delete?.bind(store);
|
|
2224
|
+
if (remove !== undefined) {
|
|
2225
|
+
await withProtectedWrite(snapshotPath, "delete", () => remove({ kind: "snapshot", key: workflowId }));
|
|
2226
|
+
}
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
try {
|
|
2230
|
+
if (readdirSync2(workflowDir).length === 0) {
|
|
2231
|
+
rmdirSync2(workflowDir);
|
|
2232
|
+
}
|
|
2233
|
+
} catch {}
|
|
2234
|
+
throw error;
|
|
2235
|
+
}
|
|
2236
|
+
return { workflowId, snapshotPath, recovered: false };
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2026
2239
|
|
|
2027
2240
|
// src/status.ts
|
|
2028
2241
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -7566,6 +7779,176 @@ function slugify(title) {
|
|
|
7566
7779
|
}
|
|
7567
7780
|
var escapeCell = (value) => value.replace(/\|/g, "\\|");
|
|
7568
7781
|
var truncate = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value;
|
|
7782
|
+
var collapseEvidenceWs = (value) => value.replace(/\s*[\r\n]\s*/g, " ");
|
|
7783
|
+
var evidenceText = (item) => typeof item === "string" ? item : `${item.file}${item.line !== undefined ? `:${item.line}` : ""} — ${item.description}`;
|
|
7784
|
+
var hasEnrichedMetadata = (finding) => finding.fingerprint !== undefined || finding.trace !== undefined || finding.severity !== undefined || finding.evidence.some((item) => typeof item !== "string");
|
|
7785
|
+
var AUDIT_FINGERPRINT_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]*(?![\s\S])/;
|
|
7786
|
+
var AUDIT_SEVERITY_ORDER = { informational: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
7787
|
+
var DEFAULT_IGNORABLE_RE = /[\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180F\u200B-\u200F\u202A-\u202E\u2060-\u206F\u3164\uFE00-\uFE0F\uFEFF\uFFA0\uFFF0-\uFFF8\u{1BCA0}-\u{1BCA3}\u{1D173}-\u{1D17A}\u{E0000}-\u{E0FFF}]/u;
|
|
7788
|
+
var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
7789
|
+
var isPlainObject9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7790
|
+
function isVisibleText(value) {
|
|
7791
|
+
if (LONE_SURROGATE_RE.test(value))
|
|
7792
|
+
return false;
|
|
7793
|
+
for (const ch of value) {
|
|
7794
|
+
if (!/\p{White_Space}/u.test(ch) && !DEFAULT_IGNORABLE_RE.test(ch))
|
|
7795
|
+
return true;
|
|
7796
|
+
}
|
|
7797
|
+
return false;
|
|
7798
|
+
}
|
|
7799
|
+
function safeAuditPath(value) {
|
|
7800
|
+
if (typeof value !== "string")
|
|
7801
|
+
return false;
|
|
7802
|
+
if (value === "")
|
|
7803
|
+
return false;
|
|
7804
|
+
if (value.includes("\\"))
|
|
7805
|
+
return false;
|
|
7806
|
+
if (/[\u0000-\u001F\u007F-\u009F]/.test(value))
|
|
7807
|
+
return false;
|
|
7808
|
+
if (LONE_SURROGATE_RE.test(value))
|
|
7809
|
+
return false;
|
|
7810
|
+
if (value.startsWith("/"))
|
|
7811
|
+
return false;
|
|
7812
|
+
if (/^[A-Za-z]:/.test(value))
|
|
7813
|
+
return false;
|
|
7814
|
+
for (const segment of value.split("/")) {
|
|
7815
|
+
if (segment === "" || segment === "." || segment === "..")
|
|
7816
|
+
return false;
|
|
7817
|
+
if (segment.endsWith(".") || segment.endsWith(" "))
|
|
7818
|
+
return false;
|
|
7819
|
+
}
|
|
7820
|
+
return true;
|
|
7821
|
+
}
|
|
7822
|
+
function validateAuditFindingGates(findings) {
|
|
7823
|
+
const violations = [];
|
|
7824
|
+
const seen = new Map;
|
|
7825
|
+
let previousFingerprint;
|
|
7826
|
+
if (!Array.isArray(findings)) {
|
|
7827
|
+
violations.push(violation10("high", "audit.finding.shape", "findings — audit.finding.shape"));
|
|
7828
|
+
return { ok: false, violations };
|
|
7829
|
+
}
|
|
7830
|
+
findings.forEach((finding, index) => {
|
|
7831
|
+
const at = (field) => `findings[${index}].${field}`;
|
|
7832
|
+
const push = (code, field) => {
|
|
7833
|
+
violations.push(violation10("high", code, `${at(field)} — ${code}`));
|
|
7834
|
+
};
|
|
7835
|
+
if (!isPlainObject9(finding)) {
|
|
7836
|
+
violations.push(violation10("high", "audit.finding.shape", `findings[${index}] — audit.finding.shape`));
|
|
7837
|
+
return;
|
|
7838
|
+
}
|
|
7839
|
+
if (finding.fingerprint !== undefined) {
|
|
7840
|
+
const fingerprint = finding.fingerprint;
|
|
7841
|
+
if (typeof fingerprint !== "string" || !AUDIT_FINGERPRINT_RE.test(fingerprint))
|
|
7842
|
+
push("audit.finding.fingerprint.grammar", "fingerprint");
|
|
7843
|
+
else if (redactSecrets(fingerprint).text !== fingerprint)
|
|
7844
|
+
push("audit.finding.fingerprint.secret", "fingerprint");
|
|
7845
|
+
else {
|
|
7846
|
+
const firstAt = seen.get(fingerprint);
|
|
7847
|
+
if (firstAt !== undefined)
|
|
7848
|
+
push("audit.finding.fingerprint.duplicate", "fingerprint");
|
|
7849
|
+
else
|
|
7850
|
+
seen.set(fingerprint, index);
|
|
7851
|
+
if (previousFingerprint !== undefined && fingerprint < previousFingerprint) {
|
|
7852
|
+
push("audit.finding.fingerprint.order", "fingerprint");
|
|
7853
|
+
}
|
|
7854
|
+
previousFingerprint = fingerprint;
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
7857
|
+
if (finding.severity !== undefined) {
|
|
7858
|
+
if (!isPlainObject9(finding.severity)) {
|
|
7859
|
+
push("audit.finding.severity.shape", "severity");
|
|
7860
|
+
} else {
|
|
7861
|
+
const { likelihood, impact, overall } = finding.severity;
|
|
7862
|
+
const rankOf = (r) => AUDIT_SEVERITY_ORDER[r];
|
|
7863
|
+
for (const [field, value] of [
|
|
7864
|
+
["likelihood", likelihood],
|
|
7865
|
+
["impact", impact],
|
|
7866
|
+
["overall", overall]
|
|
7867
|
+
]) {
|
|
7868
|
+
if (rankOf(value) === undefined)
|
|
7869
|
+
push("audit.finding.severity.rank", `severity.${field}`);
|
|
7870
|
+
}
|
|
7871
|
+
const impactRank = rankOf(impact);
|
|
7872
|
+
const overallRank = rankOf(overall);
|
|
7873
|
+
if (impactRank !== undefined && overallRank !== undefined && overallRank > impactRank) {
|
|
7874
|
+
push("audit.finding.severity.overall-exceeds-impact", "severity.overall");
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
}
|
|
7878
|
+
const textViolation = (field, value) => {
|
|
7879
|
+
if (value === undefined)
|
|
7880
|
+
return;
|
|
7881
|
+
if (typeof value !== "string")
|
|
7882
|
+
push("audit.finding.text.type", field);
|
|
7883
|
+
else if (LONE_SURROGATE_RE.test(value))
|
|
7884
|
+
push("audit.finding.text.surrogate", field);
|
|
7885
|
+
else if (!isVisibleText(value))
|
|
7886
|
+
push("audit.finding.text.invisible", field);
|
|
7887
|
+
};
|
|
7888
|
+
if (finding.trace !== undefined) {
|
|
7889
|
+
if (!Array.isArray(finding.trace))
|
|
7890
|
+
push("audit.finding.trace.shape", "trace");
|
|
7891
|
+
else if (finding.trace.length === 0)
|
|
7892
|
+
push("audit.finding.trace.empty", "trace");
|
|
7893
|
+
else {
|
|
7894
|
+
const kinds = finding.trace.map((s) => isPlainObject9(s) ? s.kind : undefined);
|
|
7895
|
+
const topologyOk = finding.trace.length === 1 ? kinds[0] === "entrypoint" || kinds[0] === "sink" : kinds[0] === "entrypoint" && kinds[kinds.length - 1] === "sink" && kinds.slice(1, -1).every((k) => k === "propagation");
|
|
7896
|
+
if (!topologyOk)
|
|
7897
|
+
push("audit.finding.trace.topology", "trace");
|
|
7898
|
+
finding.trace.forEach((step, stepIndex) => {
|
|
7899
|
+
const stepAt = `trace[${stepIndex}]`;
|
|
7900
|
+
if (!isPlainObject9(step)) {
|
|
7901
|
+
push("audit.finding.trace.shape", stepAt);
|
|
7902
|
+
return;
|
|
7903
|
+
}
|
|
7904
|
+
if (typeof step.line !== "number" || !Number.isSafeInteger(step.line) || step.line <= 0) {
|
|
7905
|
+
push("audit.finding.trace.line", `${stepAt}.line`);
|
|
7906
|
+
}
|
|
7907
|
+
const stepFile = step.file;
|
|
7908
|
+
if (typeof stepFile !== "string" || !safeAuditPath(stepFile))
|
|
7909
|
+
push("audit.finding.path.unsafe", `${stepAt}.file`);
|
|
7910
|
+
else if (redactSecrets(stepFile).text !== stepFile)
|
|
7911
|
+
push("audit.finding.path.secret", `${stepAt}.file`);
|
|
7912
|
+
if (step.scope === undefined)
|
|
7913
|
+
push("audit.finding.trace.shape", `${stepAt}.scope`);
|
|
7914
|
+
else
|
|
7915
|
+
textViolation(`${stepAt}.scope`, step.scope);
|
|
7916
|
+
if (step.description === undefined)
|
|
7917
|
+
push("audit.finding.trace.shape", `${stepAt}.description`);
|
|
7918
|
+
else
|
|
7919
|
+
textViolation(`${stepAt}.description`, step.description);
|
|
7920
|
+
});
|
|
7921
|
+
}
|
|
7922
|
+
}
|
|
7923
|
+
if (!Array.isArray(finding.evidence))
|
|
7924
|
+
push("audit.finding.evidence.shape", "evidence");
|
|
7925
|
+
else
|
|
7926
|
+
finding.evidence.forEach((item, itemIndex) => {
|
|
7927
|
+
if (typeof item === "string") {
|
|
7928
|
+
textViolation(`evidence[${itemIndex}]`, item);
|
|
7929
|
+
return;
|
|
7930
|
+
}
|
|
7931
|
+
if (!isPlainObject9(item)) {
|
|
7932
|
+
push("audit.finding.evidence.shape", `evidence[${itemIndex}]`);
|
|
7933
|
+
return;
|
|
7934
|
+
}
|
|
7935
|
+
const itemFile = item.file;
|
|
7936
|
+
if (typeof itemFile !== "string" || !safeAuditPath(itemFile))
|
|
7937
|
+
push("audit.finding.path.unsafe", `evidence[${itemIndex}].file`);
|
|
7938
|
+
else if (redactSecrets(itemFile).text !== itemFile)
|
|
7939
|
+
push("audit.finding.path.secret", `evidence[${itemIndex}].file`);
|
|
7940
|
+
if (item.line !== undefined && (typeof item.line !== "number" || !Number.isSafeInteger(item.line) || item.line <= 0)) {
|
|
7941
|
+
push("audit.finding.evidence.line", `evidence[${itemIndex}].line`);
|
|
7942
|
+
}
|
|
7943
|
+
textViolation(`evidence[${itemIndex}].description`, item.description);
|
|
7944
|
+
});
|
|
7945
|
+
textViolation("title", finding.title);
|
|
7946
|
+
textViolation("impact", finding.impact);
|
|
7947
|
+
textViolation("fixSketch", finding.fixSketch);
|
|
7948
|
+
textViolation("verification", finding.verification);
|
|
7949
|
+
});
|
|
7950
|
+
return { ok: violations.length === 0, violations };
|
|
7951
|
+
}
|
|
7569
7952
|
function renderPlanFile(finding, plannedAt) {
|
|
7570
7953
|
const sections = [
|
|
7571
7954
|
`# ${finding.title}`,
|
|
@@ -7574,15 +7957,22 @@ function renderPlanFile(finding, plannedAt) {
|
|
|
7574
7957
|
`- **Priority**: ${finding.priority}`,
|
|
7575
7958
|
`- **Effort**: ${finding.effort}`,
|
|
7576
7959
|
`- **Risk**: ${finding.risk}`,
|
|
7960
|
+
...finding.confidence !== "MED" || hasEnrichedMetadata(finding) ? [`- **Confidence**: ${finding.confidence}`] : [],
|
|
7961
|
+
...finding.fingerprint !== undefined ? [`- **Fingerprint**: ${finding.fingerprint}`] : [],
|
|
7962
|
+
...finding.severity !== undefined ? [`- **Likelihood**: ${finding.severity.likelihood}`, `- **Severity impact**: ${finding.severity.impact}`, `- **Severity**: ${finding.severity.overall}`] : [],
|
|
7577
7963
|
`- **Depends on**: ${finding.dependsOn ?? "none"}`,
|
|
7578
7964
|
`- **Category**: ${finding.category}`,
|
|
7965
|
+
...finding.evidence.length > 0 ? [`- **Evidence**: ${collapseEvidenceWs(evidenceText(finding.evidence[0]))}`] : [],
|
|
7579
7966
|
`- **Planned at**: commit \`${plannedAt.commit}\`, ${plannedAt.date}`,
|
|
7580
7967
|
"",
|
|
7581
7968
|
"## Impact",
|
|
7582
7969
|
finding.impact
|
|
7583
7970
|
];
|
|
7584
7971
|
if (finding.evidence.length > 0) {
|
|
7585
|
-
sections.push("", "## Evidence", ...finding.evidence.map((
|
|
7972
|
+
sections.push("", "## Evidence", ...finding.evidence.map((item) => `- ${evidenceText(item)}`));
|
|
7973
|
+
}
|
|
7974
|
+
if (finding.trace !== undefined) {
|
|
7975
|
+
sections.push("", "## Trace", "", "| Kind | Location | Scope / Description |", "|------|----------|---------------------|", ...finding.trace.map((step) => `| ${escapeCell(step.kind)} | ${escapeCell(`${step.file}:${step.line}`)} | ${escapeCell(`${step.scope} — ${step.description}`).replace(/\r\n|\r|\n/g, "\\n")} |`));
|
|
7586
7976
|
}
|
|
7587
7977
|
if (finding.fixSketch !== undefined) {
|
|
7588
7978
|
sections.push("", "## Fix sketch", finding.fixSketch);
|
|
@@ -7602,7 +7992,8 @@ function redactFinding(finding) {
|
|
|
7602
7992
|
...finding,
|
|
7603
7993
|
title: redactText(finding.title),
|
|
7604
7994
|
impact: redactText(finding.impact),
|
|
7605
|
-
evidence: finding.evidence.map(redactText),
|
|
7995
|
+
evidence: finding.evidence.map((item) => typeof item === "string" ? redactText(item) : { ...item, description: redactText(item.description) }),
|
|
7996
|
+
...finding.trace !== undefined ? { trace: finding.trace.map((step) => ({ ...step, scope: redactText(step.scope), description: redactText(step.description) })) } : {},
|
|
7606
7997
|
...finding.fixSketch !== undefined ? { fixSketch: redactText(finding.fixSketch) } : {},
|
|
7607
7998
|
...finding.verification !== undefined ? { verification: redactText(finding.verification) } : {}
|
|
7608
7999
|
};
|
|
@@ -7624,7 +8015,10 @@ function extractSecurityDispositionSections(text) {
|
|
|
7624
8015
|
}
|
|
7625
8016
|
function renderIndex(params) {
|
|
7626
8017
|
const { date, repoName, repoShortSha, rows, rejected, needsVerification, hardeningChecked } = params;
|
|
7627
|
-
const
|
|
8018
|
+
const showFingerprint = rows.some((r) => r.fingerprint !== undefined);
|
|
8019
|
+
const showSeverity = rows.some((r) => r.likelihood !== undefined || r.severityImpact !== undefined || r.severity !== undefined);
|
|
8020
|
+
const cell = (value) => escapeCell(value ?? "—");
|
|
8021
|
+
const findingsRows = rows.map((r) => `| ${r.num} | ${escapeCell(r.title)} | ${r.category} | ${escapeCell(truncate(r.impact, 80))} | ${r.effort} | ${r.risk} | ${r.confidence} | ${escapeCell(truncate(r.evidence, 80))}` + (showFingerprint ? ` | ${cell(r.fingerprint)}` : "") + (showSeverity ? ` | ${cell(r.likelihood)} | ${cell(r.severityImpact)} | ${cell(r.severity)}` : "") + " |").join(`
|
|
7628
8022
|
`);
|
|
7629
8023
|
const directionRows = rows.filter((r) => r.category === "direction").map((r) => `- ${escapeCell(r.title)} — ${escapeCell(truncate(r.impact, 120))}`).join(`
|
|
7630
8024
|
`);
|
|
@@ -7637,8 +8031,8 @@ function renderIndex(params) {
|
|
|
7637
8031
|
"",
|
|
7638
8032
|
"## Findings",
|
|
7639
8033
|
"",
|
|
7640
|
-
"| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence |",
|
|
7641
|
-
"
|
|
8034
|
+
"| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence" + (showFingerprint ? " | Fingerprint" : "") + (showSeverity ? " | Likelihood | Severity impact | Severity" : "") + " |",
|
|
8035
|
+
"|---|---------|----------|--------|--------|------|------------|----------" + (showFingerprint ? "|------------" : "") + (showSeverity ? "|------------|-----------------|----------" : "") + "|",
|
|
7642
8036
|
findingsRows
|
|
7643
8037
|
];
|
|
7644
8038
|
if (directionRows !== "") {
|
|
@@ -7662,6 +8056,11 @@ function renderIndex(params) {
|
|
|
7662
8056
|
function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
7663
8057
|
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
7664
8058
|
const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
|
|
8059
|
+
const gate2 = validateAuditFindingGates(findings);
|
|
8060
|
+
if (!gate2.ok) {
|
|
8061
|
+
const first = gate2.violations[0];
|
|
8062
|
+
throw new TypeError(`invalid audit findings — ${first.code}: ${first.message}`);
|
|
8063
|
+
}
|
|
7665
8064
|
mkdirSync8(outDir, { recursive: true });
|
|
7666
8065
|
const existingReadme = join16(outDir, "README.md");
|
|
7667
8066
|
const carried = existsSync12(existingReadme) ? extractSecurityDispositionSections(readFileSync11(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
|
|
@@ -7696,10 +8095,14 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
7696
8095
|
impact: "see plan file",
|
|
7697
8096
|
effort: fields.get("Effort") ?? "—",
|
|
7698
8097
|
risk: fields.get("Risk") ?? "—",
|
|
7699
|
-
confidence: "—",
|
|
8098
|
+
confidence: fields.get("Confidence") ?? "—",
|
|
7700
8099
|
evidence: fields.get("Evidence") ?? "—",
|
|
7701
8100
|
priority: fields.get("Priority") ?? "—",
|
|
7702
|
-
dependsOn: fields.get("Depends on") ?? "—"
|
|
8101
|
+
dependsOn: fields.get("Depends on") ?? "—",
|
|
8102
|
+
fingerprint: fields.get("Fingerprint"),
|
|
8103
|
+
likelihood: fields.get("Likelihood"),
|
|
8104
|
+
severityImpact: fields.get("Severity impact"),
|
|
8105
|
+
severity: fields.get("Severity")
|
|
7703
8106
|
};
|
|
7704
8107
|
});
|
|
7705
8108
|
const byNum = new Map(rows.map((r) => [r.num, r]));
|
|
@@ -7714,9 +8117,13 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
7714
8117
|
row.effort = finding.effort;
|
|
7715
8118
|
row.risk = finding.risk;
|
|
7716
8119
|
row.confidence = finding.confidence;
|
|
7717
|
-
row.evidence = finding.evidence[0]
|
|
8120
|
+
row.evidence = finding.evidence.length > 0 ? collapseEvidenceWs(evidenceText(finding.evidence[0])) : "";
|
|
7718
8121
|
row.priority = finding.priority;
|
|
7719
8122
|
row.dependsOn = finding.dependsOn ?? "none";
|
|
8123
|
+
row.fingerprint = finding.fingerprint;
|
|
8124
|
+
row.likelihood = finding.severity?.likelihood;
|
|
8125
|
+
row.severityImpact = finding.severity?.impact;
|
|
8126
|
+
row.severity = finding.severity?.overall;
|
|
7720
8127
|
}
|
|
7721
8128
|
});
|
|
7722
8129
|
const needsVerificationLines = options.needsVerification !== undefined ? options.needsVerification.map((nv) => `- ${escapeCell(redactText(nv.lead))}: ${escapeCell(redactText(nv.how))}${nv.evidence ? ` (${escapeCell(redactText(nv.evidence))})` : ""}`) : carried.needsVerification;
|
|
@@ -8952,7 +9359,7 @@ function computePrTally(input) {
|
|
|
8952
9359
|
var REVIEW_SCHEMA_ID = "mstar.review/v1";
|
|
8953
9360
|
var INSPECTOR_VERDICTS = ["comment", "request_changes", "approve"];
|
|
8954
9361
|
var INSPECTOR_SEVERITIES = ["critical", "warning", "suggestion", "info"];
|
|
8955
|
-
function
|
|
9362
|
+
function isPlainObject10(value) {
|
|
8956
9363
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8957
9364
|
}
|
|
8958
9365
|
var TALLY_COUNT_KEYS = ["mustFix", "shouldFix", "nit", "unverified"];
|
|
@@ -8963,7 +9370,7 @@ function checkProvidedTallyShape(tally, violations) {
|
|
|
8963
9370
|
if (typeof tally.scorePct !== "number" || !Number.isInteger(tally.scorePct) || tally.scorePct < 0 || tally.scorePct > 100) {
|
|
8964
9371
|
violations.push(violation15("high", "review.tally-malformed", `tally.scorePct must be an integer in [0, 100] - got ${String(tally.scorePct)}`));
|
|
8965
9372
|
}
|
|
8966
|
-
if (!
|
|
9373
|
+
if (!isPlainObject10(tally.tally)) {
|
|
8967
9374
|
violations.push(violation15("high", "review.tally-malformed", "tally.tally must be an object carrying the four class counts"));
|
|
8968
9375
|
} else {
|
|
8969
9376
|
for (const key of TALLY_COUNT_KEYS) {
|
|
@@ -8979,7 +9386,7 @@ function checkProvidedTallyShape(tally, violations) {
|
|
|
8979
9386
|
}
|
|
8980
9387
|
function validateMstarReviewV1(doc) {
|
|
8981
9388
|
const violations = [];
|
|
8982
|
-
if (!
|
|
9389
|
+
if (!isPlainObject10(doc)) {
|
|
8983
9390
|
return {
|
|
8984
9391
|
ok: false,
|
|
8985
9392
|
violations: [violation15("high", "review.not-object", "review document must be a JSON object")]
|
|
@@ -9006,7 +9413,7 @@ function validateMstarReviewV1(doc) {
|
|
|
9006
9413
|
violations.push(violation15("high", "review.findings-not-array", "findings must be an array"));
|
|
9007
9414
|
} else {
|
|
9008
9415
|
doc.findings.forEach((finding, index) => {
|
|
9009
|
-
if (!
|
|
9416
|
+
if (!isPlainObject10(finding)) {
|
|
9010
9417
|
violations.push(violation15("high", "review.invalid-finding", `findings[${index}] must be an object`));
|
|
9011
9418
|
return;
|
|
9012
9419
|
}
|
|
@@ -9046,7 +9453,7 @@ function validateMstarReviewV1(doc) {
|
|
|
9046
9453
|
});
|
|
9047
9454
|
}
|
|
9048
9455
|
if (doc.tally !== undefined) {
|
|
9049
|
-
if (!
|
|
9456
|
+
if (!isPlainObject10(doc.tally)) {
|
|
9050
9457
|
violations.push(violation15("high", "review.invalid-tally", "tally must be a PrTallyResult object"));
|
|
9051
9458
|
} else {
|
|
9052
9459
|
checkProvidedTallyShape(doc.tally, violations);
|
|
@@ -9056,7 +9463,7 @@ function validateMstarReviewV1(doc) {
|
|
|
9056
9463
|
}
|
|
9057
9464
|
}
|
|
9058
9465
|
if (doc.target !== undefined) {
|
|
9059
|
-
if (!
|
|
9466
|
+
if (!isPlainObject10(doc.target)) {
|
|
9060
9467
|
violations.push(violation15("high", "review.invalid-target", "target must be an object"));
|
|
9061
9468
|
} else {
|
|
9062
9469
|
if (doc.target.owner !== undefined && typeof doc.target.owner !== "string") {
|
|
@@ -9906,6 +10313,7 @@ var IMPLEMENTED_OPERATIONS = {
|
|
|
9906
10313
|
"integration-start": true,
|
|
9907
10314
|
"integration-accept": true,
|
|
9908
10315
|
complete: true,
|
|
10316
|
+
"repair-delivery-source": true,
|
|
9909
10317
|
reconcile: true
|
|
9910
10318
|
};
|
|
9911
10319
|
function nowIso() {
|
|
@@ -10465,7 +10873,11 @@ function allowedOperations(role, sessionId, snapshot, row) {
|
|
|
10465
10873
|
out.push("accept", "return");
|
|
10466
10874
|
break;
|
|
10467
10875
|
case "accepted":
|
|
10468
|
-
|
|
10876
|
+
if (isStandaloneDevelopmentWorkflow(snapshot)) {
|
|
10877
|
+
out.push("return", "complete", "repair-delivery-source");
|
|
10878
|
+
} else {
|
|
10879
|
+
out.push("return", "integration-start");
|
|
10880
|
+
}
|
|
10469
10881
|
break;
|
|
10470
10882
|
case "integrating":
|
|
10471
10883
|
out.push("integration-accept", "complete", "reconcile");
|
|
@@ -11240,6 +11652,13 @@ async function mutatePlanCoordination(request) {
|
|
|
11240
11652
|
expectedRevision
|
|
11241
11653
|
});
|
|
11242
11654
|
}
|
|
11655
|
+
case "repair-delivery-source": {
|
|
11656
|
+
assertExactKeys(operation, ["kind", "handoffId"], "repair-delivery-source operation");
|
|
11657
|
+
return mutateRepairDeliverySource(await coordinatorScope(session, request.planId, kind), session, sessionAbs, {
|
|
11658
|
+
handoffId: namedHandoffId(operation),
|
|
11659
|
+
expectedRevision
|
|
11660
|
+
});
|
|
11661
|
+
}
|
|
11243
11662
|
case "reconcile": {
|
|
11244
11663
|
assertExactKeys(operation, ["kind", "handoffId"], "reconcile operation");
|
|
11245
11664
|
return mutateReconcile(await coordinatorScope(session, request.planId, kind), session, sessionAbs, {
|
|
@@ -11270,6 +11689,7 @@ var COORDINATOR_OPERATIONS = [
|
|
|
11270
11689
|
"integration-start",
|
|
11271
11690
|
"integration-accept",
|
|
11272
11691
|
"complete",
|
|
11692
|
+
"repair-delivery-source",
|
|
11273
11693
|
"reconcile"
|
|
11274
11694
|
];
|
|
11275
11695
|
function assertSessionRole(session, kind) {
|
|
@@ -11910,6 +12330,313 @@ async function mutateIntegrationAccept(scope, session, sessionPath, request) {
|
|
|
11910
12330
|
view: buildView(scope.harnessRoot, scope.workflowId, scope.projectId, scope, result.snapshot, result.row, session, sessionPath)
|
|
11911
12331
|
};
|
|
11912
12332
|
}
|
|
12333
|
+
function validateRowCoordinationInContext(context, coordination, what) {
|
|
12334
|
+
assertViolationFree(validateRowCoordination(coordination, what, rowValidationRoute(context.snapshot, context.row)), what);
|
|
12335
|
+
}
|
|
12336
|
+
function standaloneDeliveryAnchors(snapshot, planId) {
|
|
12337
|
+
const source = snapshot.branch?.source;
|
|
12338
|
+
const target = snapshot.branch?.target;
|
|
12339
|
+
if (!isNonEmptyString(source) || !isNonEmptyString(target)) {
|
|
12340
|
+
throw new CoordinationError("coordination.invalid-transition", `plan ${planId} has no delivery anchors — the snapshot must name branch.source and branch.target`, { plan_id: planId });
|
|
12341
|
+
}
|
|
12342
|
+
return { source, target };
|
|
12343
|
+
}
|
|
12344
|
+
function assertStandaloneRoute(snapshot, planId, what) {
|
|
12345
|
+
if (!isStandaloneDevelopmentWorkflow(snapshot)) {
|
|
12346
|
+
throw new CoordinationError("coordination.invalid-transition", `${what} requires a standalone development workflow for plan ${planId}`, { plan_id: planId });
|
|
12347
|
+
}
|
|
12348
|
+
}
|
|
12349
|
+
function assertNoIntegrationContamination(context, planId, handoff, what, code = "coordination.invalid-transition") {
|
|
12350
|
+
if (handoff.integration !== undefined) {
|
|
12351
|
+
throw new CoordinationError(code, `${what} refuses plan ${planId} because the handoff already carries an integration record`, { plan_id: planId });
|
|
12352
|
+
}
|
|
12353
|
+
if (context.snapshot.integration_worktree_path !== undefined) {
|
|
12354
|
+
throw new CoordinationError(code, `${what} refuses plan ${planId} because the snapshot names integration_worktree_path`, { plan_id: planId });
|
|
12355
|
+
}
|
|
12356
|
+
if (isNonEmptyString(context.snapshot.branch?.integration)) {
|
|
12357
|
+
throw new CoordinationError(code, `${what} refuses plan ${planId} because the snapshot names branch.integration`, { plan_id: planId, integration: context.snapshot.branch?.integration });
|
|
12358
|
+
}
|
|
12359
|
+
if (context.snapshot.integration_merge_lease !== undefined) {
|
|
12360
|
+
throw new CoordinationError(code, `${what} refuses plan ${planId} because the snapshot carries an integration merge lease`, { plan_id: planId });
|
|
12361
|
+
}
|
|
12362
|
+
}
|
|
12363
|
+
function assertAcceptedReviewDecision(handoff, planId, what) {
|
|
12364
|
+
if (handoff.qc.decision !== "Approve" && handoff.qc.decision !== "Approve with residuals") {
|
|
12365
|
+
throw new CoordinationError("coordination.invalid-transition", `${what} requires an accepted QC decision for plan ${planId} — got ${handoff.qc.decision}`, { plan_id: planId, decision: handoff.qc.decision });
|
|
12366
|
+
}
|
|
12367
|
+
if (handoff.qa.decision !== "pass") {
|
|
12368
|
+
throw new CoordinationError("coordination.invalid-transition", `${what} requires QA decision pass for plan ${planId} — got ${handoff.qa.decision}`, { plan_id: planId, decision: handoff.qa.decision });
|
|
12369
|
+
}
|
|
12370
|
+
}
|
|
12371
|
+
function assertStandaloneBranchIdentity(context, scope, handoff, anchors, what, requireLease = true) {
|
|
12372
|
+
const worktree = canonicalTarget(scope.worktreePath);
|
|
12373
|
+
if (handoff.source_branch !== anchors.source) {
|
|
12374
|
+
throw new CoordinationError("coordination.scope-mismatch", `${what} requires handoff.source_branch ${handoff.source_branch} to equal the registered delivery source ${anchors.source}`, { plan_id: scope.planId, expected: anchors.source, actual: handoff.source_branch });
|
|
12375
|
+
}
|
|
12376
|
+
if (requireLease) {
|
|
12377
|
+
const lease = requireExecutionLease(context.row, scope.planId, what);
|
|
12378
|
+
if (scope.workingBranch !== anchors.source) {
|
|
12379
|
+
throw new CoordinationError("coordination.scope-mismatch", `${what} requires the prepared working branch ${scope.workingBranch} to equal the registered delivery source ${anchors.source}`, { plan_id: scope.planId, expected: anchors.source, actual: scope.workingBranch });
|
|
12380
|
+
}
|
|
12381
|
+
if (lease.working_branch !== anchors.source) {
|
|
12382
|
+
throw new CoordinationError("coordination.scope-mismatch", `${what} requires the execution lease working branch ${lease.working_branch} to equal the registered delivery source ${anchors.source}`, { plan_id: scope.planId, expected: anchors.source, actual: lease.working_branch });
|
|
12383
|
+
}
|
|
12384
|
+
if (canonicalTarget(lease.worktree_path) !== worktree) {
|
|
12385
|
+
throw new CoordinationError("coordination.path-mismatch", `${what} requires the execution lease worktree ${lease.worktree_path} to equal the prepared scope ${worktree}`, { plan_id: scope.planId, expected: worktree, actual: lease.worktree_path });
|
|
12386
|
+
}
|
|
12387
|
+
}
|
|
12388
|
+
if (canonicalTarget(handoff.worktree_path) !== worktree) {
|
|
12389
|
+
throw new CoordinationError("coordination.path-mismatch", `${what} requires the handoff worktree ${handoff.worktree_path} to equal the prepared scope ${worktree}`, { plan_id: scope.planId, expected: worktree, actual: handoff.worktree_path });
|
|
12390
|
+
}
|
|
12391
|
+
const metadata = context.row.metadata;
|
|
12392
|
+
if (isPlainObject2(metadata) && metadata.working_branch !== undefined && metadata.working_branch !== anchors.source) {
|
|
12393
|
+
throw new CoordinationError("coordination.scope-mismatch", `${what} requires row metadata.working_branch to equal the registered delivery source ${anchors.source}`, { plan_id: scope.planId, expected: anchors.source, actual: metadata.working_branch });
|
|
12394
|
+
}
|
|
12395
|
+
if (isPlainObject2(metadata) && metadata.worktree_path !== undefined && canonicalTarget(String(metadata.worktree_path)) !== worktree) {
|
|
12396
|
+
throw new CoordinationError("coordination.path-mismatch", `${what} requires row metadata.worktree_path to equal the prepared scope ${worktree}`, { plan_id: scope.planId, expected: worktree, actual: metadata.worktree_path });
|
|
12397
|
+
}
|
|
12398
|
+
}
|
|
12399
|
+
function assertStandaloneSourceGitProof(scope, handoff, sourceBranch, what) {
|
|
12400
|
+
assertFeatureCheckout(scope, handoff.source_sha, what);
|
|
12401
|
+
const branch = gitRead(scope.worktreePath, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
12402
|
+
if (branch !== sourceBranch) {
|
|
12403
|
+
throw gitProof(`${what} requires the plan worktree ${scope.worktreePath} to be on ${sourceBranch} — got ${branch || "a detached HEAD"}`, { plan_id: scope.planId, expected: sourceBranch, actual: branch });
|
|
12404
|
+
}
|
|
12405
|
+
const refTip = gitRead(scope.worktreePath, ["rev-parse", `refs/heads/${sourceBranch}`]);
|
|
12406
|
+
if (refTip !== handoff.source_sha) {
|
|
12407
|
+
throw gitProof(`${what} requires refs/heads/${sourceBranch} to resolve to the pinned source ${handoff.source_sha} — got ${refTip || "missing"}`, { plan_id: scope.planId, expected: handoff.source_sha, actual: refTip });
|
|
12408
|
+
}
|
|
12409
|
+
if (handoff.review_head !== handoff.source_sha) {
|
|
12410
|
+
throw gitProof(`${what} requires review_head to be the pinned source ${handoff.source_sha} — got ${handoff.review_head}`, { plan_id: scope.planId, source_sha: handoff.source_sha, review_head: handoff.review_head });
|
|
12411
|
+
}
|
|
12412
|
+
if (!gitObjectExists(scope.worktreePath, handoff.review_base)) {
|
|
12413
|
+
throw gitProof(`${what} review base ${handoff.review_base} is not a commit of ${scope.worktreePath}`, {
|
|
12414
|
+
plan_id: scope.planId,
|
|
12415
|
+
review_base: handoff.review_base
|
|
12416
|
+
});
|
|
12417
|
+
}
|
|
12418
|
+
if (!gitIsAncestor(scope.worktreePath, handoff.review_base, handoff.review_head)) {
|
|
12419
|
+
throw gitProof(`${what} review range ${handoff.review_base}..${handoff.review_head} is not an ancestry`, { plan_id: scope.planId, review_base: handoff.review_base, review_head: handoff.review_head });
|
|
12420
|
+
}
|
|
12421
|
+
}
|
|
12422
|
+
async function assertStandaloneCompletionPrecheck(context, scope, session, handoff) {
|
|
12423
|
+
assertStandaloneRoute(context.snapshot, scope.planId, "complete");
|
|
12424
|
+
if (context.snapshot.status !== "running") {
|
|
12425
|
+
throw new CoordinationError("coordination.invalid-transition", `complete requires workflow ${context.snapshot.id} to still be running — got ${context.snapshot.status}`, { workflow_id: context.snapshot.id, status: context.snapshot.status });
|
|
12426
|
+
}
|
|
12427
|
+
if (handoff.state !== "accepted") {
|
|
12428
|
+
throw new CoordinationError("coordination.invalid-transition", `plan ${scope.planId} handoff is ${handoff.state} — standalone complete requires an accepted handoff`, { plan_id: scope.planId, state: handoff.state });
|
|
12429
|
+
}
|
|
12430
|
+
if (rowStatusOf(context.row) !== "InReview") {
|
|
12431
|
+
throw new CoordinationError("coordination.invalid-transition", `complete requires ${scope.planId} to still be InReview`, { plan_id: scope.planId, status: context.row.status });
|
|
12432
|
+
}
|
|
12433
|
+
const prepared = context.coordination?.prepared;
|
|
12434
|
+
if (prepared === undefined) {
|
|
12435
|
+
throw new CoordinationError("coordination.not-prepared", `plan ${scope.planId} is not prepared in this workflow`, { plan_id: scope.planId });
|
|
12436
|
+
}
|
|
12437
|
+
assertNoIntegrationContamination(context, scope.planId, handoff, "complete");
|
|
12438
|
+
assertEvidenceDigests(handoff);
|
|
12439
|
+
assertAcceptedReviewDecision(handoff, scope.planId, "complete");
|
|
12440
|
+
if (handoff.qa.gate !== prepared.qa_gate) {
|
|
12441
|
+
throw new CoordinationError("coordination.assignment-stale", `complete qa.gate ${handoff.qa.gate} is not the Assignment's QA gate ${prepared.qa_gate}`, { plan_id: scope.planId, expected: prepared.qa_gate, actual: handoff.qa.gate });
|
|
12442
|
+
}
|
|
12443
|
+
await assertFindingsClosed(scope, prepared, "complete");
|
|
12444
|
+
assertExecutionHolder(context.row, session.session_id, scope.planId, "complete");
|
|
12445
|
+
const anchors = standaloneDeliveryAnchors(context.snapshot, scope.planId);
|
|
12446
|
+
assertStandaloneBranchIdentity(context, scope, handoff, anchors, "complete");
|
|
12447
|
+
assertStandaloneSourceGitProof(scope, handoff, anchors.source, "complete");
|
|
12448
|
+
}
|
|
12449
|
+
function requireAcceptedHandoffForRepair(context, planId, namedHandoffId2) {
|
|
12450
|
+
const handoff = context.coordination?.handoff;
|
|
12451
|
+
if (handoff === undefined || handoff.id !== namedHandoffId2 || handoff.state !== "accepted" || rowStatusOf(context.row) !== "InReview") {
|
|
12452
|
+
throw new CoordinationError("coordination.delivery-source-repair.no-accepted-handoff", `repair-delivery-source requires plan ${planId} to carry the named accepted handoff while InReview`, { plan_id: planId, handoff_id: namedHandoffId2, state: handoff?.state, status: context.row.status });
|
|
12453
|
+
}
|
|
12454
|
+
return handoff;
|
|
12455
|
+
}
|
|
12456
|
+
function assertRepairNotTerminal(snapshot) {
|
|
12457
|
+
if (WORKFLOW_TERMINAL_STATUSES.includes(snapshot.status)) {
|
|
12458
|
+
throw new CoordinationError("coordination.delivery-source-repair.terminal", `repair-delivery-source refuses workflow ${snapshot.id} in terminal status ${snapshot.status}`, { workflow_id: snapshot.id, status: snapshot.status });
|
|
12459
|
+
}
|
|
12460
|
+
}
|
|
12461
|
+
function assertLegacyRepairShape(snapshot, planId, handoff) {
|
|
12462
|
+
const source = snapshot.branch?.source;
|
|
12463
|
+
const target = snapshot.branch?.target;
|
|
12464
|
+
if (!isNonEmptyString(source) || !isNonEmptyString(target)) {
|
|
12465
|
+
throw new CoordinationError("coordination.delivery-source-repair.not-legacy-shape", `repair-delivery-source requires nonblank delivery anchors on plan ${planId}`, { plan_id: planId });
|
|
12466
|
+
}
|
|
12467
|
+
const candidateSource = handoff.source_branch;
|
|
12468
|
+
if (!isNonEmptyString(candidateSource)) {
|
|
12469
|
+
throw new CoordinationError("coordination.delivery-source-repair.not-legacy-shape", `repair-delivery-source requires the accepted handoff to name a nonblank source branch for plan ${planId}`, { plan_id: planId });
|
|
12470
|
+
}
|
|
12471
|
+
if (source === candidateSource) {
|
|
12472
|
+
throw new CoordinationError("coordination.delivery-source-repair.already-aligned", `repair-delivery-source refuses plan ${planId} because branch.source already equals the accepted handoff source ${candidateSource}`, { plan_id: planId, source, candidate: candidateSource });
|
|
12473
|
+
}
|
|
12474
|
+
if (source !== target) {
|
|
12475
|
+
throw new CoordinationError("coordination.delivery-source-repair.not-legacy-shape", `repair-delivery-source requires the legacy shape branch.source === branch.target for plan ${planId} — got source ${source} and target ${target}`, { plan_id: planId, source, target });
|
|
12476
|
+
}
|
|
12477
|
+
return { target, candidateSource };
|
|
12478
|
+
}
|
|
12479
|
+
function assertRepairBranchIdentity(context, scope, handoff, candidateSource, what) {
|
|
12480
|
+
const worktree = canonicalTarget(scope.worktreePath);
|
|
12481
|
+
if (handoff.source_branch !== candidateSource) {
|
|
12482
|
+
throw new CoordinationError("coordination.scope-mismatch", `${what} requires handoff.source_branch ${handoff.source_branch} to equal the candidate delivery source ${candidateSource}`, { plan_id: scope.planId, expected: candidateSource, actual: handoff.source_branch });
|
|
12483
|
+
}
|
|
12484
|
+
const lease = requireExecutionLease(context.row, scope.planId, what);
|
|
12485
|
+
if (scope.workingBranch !== candidateSource) {
|
|
12486
|
+
throw new CoordinationError("coordination.scope-mismatch", `${what} requires the prepared working branch ${scope.workingBranch} to equal the candidate delivery source ${candidateSource}`, { plan_id: scope.planId, expected: candidateSource, actual: scope.workingBranch });
|
|
12487
|
+
}
|
|
12488
|
+
if (lease.working_branch !== candidateSource) {
|
|
12489
|
+
throw new CoordinationError("coordination.scope-mismatch", `${what} requires the execution lease working branch ${lease.working_branch} to equal the candidate delivery source ${candidateSource}`, { plan_id: scope.planId, expected: candidateSource, actual: lease.working_branch });
|
|
12490
|
+
}
|
|
12491
|
+
if (canonicalTarget(lease.worktree_path) !== worktree) {
|
|
12492
|
+
throw new CoordinationError("coordination.path-mismatch", `${what} requires the execution lease worktree ${lease.worktree_path} to equal the prepared scope ${worktree}`, { plan_id: scope.planId, expected: worktree, actual: lease.worktree_path });
|
|
12493
|
+
}
|
|
12494
|
+
if (canonicalTarget(handoff.worktree_path) !== worktree) {
|
|
12495
|
+
throw new CoordinationError("coordination.path-mismatch", `${what} requires the handoff worktree ${handoff.worktree_path} to equal the prepared scope ${worktree}`, { plan_id: scope.planId, expected: worktree, actual: handoff.worktree_path });
|
|
12496
|
+
}
|
|
12497
|
+
const metadata = context.row.metadata;
|
|
12498
|
+
if (isPlainObject2(metadata) && metadata.working_branch !== undefined && metadata.working_branch !== candidateSource) {
|
|
12499
|
+
throw new CoordinationError("coordination.scope-mismatch", `${what} requires row metadata.working_branch to equal the candidate delivery source ${candidateSource}`, { plan_id: scope.planId, expected: candidateSource, actual: metadata.working_branch });
|
|
12500
|
+
}
|
|
12501
|
+
if (isPlainObject2(metadata) && metadata.worktree_path !== undefined && canonicalTarget(String(metadata.worktree_path)) !== worktree) {
|
|
12502
|
+
throw new CoordinationError("coordination.path-mismatch", `${what} requires row metadata.worktree_path to equal the prepared scope ${worktree}`, { plan_id: scope.planId, expected: worktree, actual: metadata.worktree_path });
|
|
12503
|
+
}
|
|
12504
|
+
}
|
|
12505
|
+
function assertDeliveryPrCompatible(snapshot, candidateSource, registeredTarget, planId) {
|
|
12506
|
+
const pr = snapshot.delivery?.pr;
|
|
12507
|
+
if (pr === undefined)
|
|
12508
|
+
return;
|
|
12509
|
+
if (pr.head !== candidateSource || pr.target !== registeredTarget) {
|
|
12510
|
+
throw new CoordinationError("coordination.delivery-source-repair.pr-conflict", `repair-delivery-source refuses plan ${planId} because stored PR identity head ${JSON.stringify(pr.head)} target ${JSON.stringify(pr.target)} conflicts with candidate source ${candidateSource} and registered target ${registeredTarget}`, { plan_id: planId, pr_head: pr.head, pr_target: pr.target, candidate: candidateSource, target: registeredTarget });
|
|
12511
|
+
}
|
|
12512
|
+
}
|
|
12513
|
+
function assertRepairDeliverySourceAdmission(context, scope) {
|
|
12514
|
+
if (!isStandaloneDevelopmentWorkflow(context.snapshot)) {
|
|
12515
|
+
throw new CoordinationError("coordination.delivery-source-repair.unsupported-workflow", `repair-delivery-source requires a single-row standalone development workflow for plan ${scope.planId}`, { plan_id: scope.planId, workflow_id: context.snapshot.id });
|
|
12516
|
+
}
|
|
12517
|
+
assertRepairNotTerminal(context.snapshot);
|
|
12518
|
+
if (context.snapshot.status === "paused") {
|
|
12519
|
+
throw new CoordinationError("coordination.invalid-transition", `repair-delivery-source refuses paused workflow ${context.snapshot.id}`, { workflow_id: context.snapshot.id, status: context.snapshot.status });
|
|
12520
|
+
}
|
|
12521
|
+
if (context.snapshot.status !== "running") {
|
|
12522
|
+
throw new CoordinationError("coordination.invalid-transition", `repair-delivery-source requires workflow ${context.snapshot.id} to still be running — got ${context.snapshot.status}`, { workflow_id: context.snapshot.id, status: context.snapshot.status });
|
|
12523
|
+
}
|
|
12524
|
+
}
|
|
12525
|
+
async function assertRepairDeliverySourcePrecheck(context, scope, session, handoff) {
|
|
12526
|
+
assertNoIntegrationContamination(context, scope.planId, handoff, "repair-delivery-source", "coordination.delivery-source-repair.not-legacy-shape");
|
|
12527
|
+
const prepared = context.coordination?.prepared;
|
|
12528
|
+
if (prepared === undefined) {
|
|
12529
|
+
throw new CoordinationError("coordination.not-prepared", `plan ${scope.planId} is not prepared in this workflow`, { plan_id: scope.planId });
|
|
12530
|
+
}
|
|
12531
|
+
assertEvidenceDigests(handoff);
|
|
12532
|
+
assertAcceptedReviewDecision(handoff, scope.planId, "repair-delivery-source");
|
|
12533
|
+
if (handoff.qa.gate !== prepared.qa_gate) {
|
|
12534
|
+
throw new CoordinationError("coordination.assignment-stale", `repair-delivery-source qa.gate ${handoff.qa.gate} is not the Assignment's QA gate ${prepared.qa_gate}`, { plan_id: scope.planId, expected: prepared.qa_gate, actual: handoff.qa.gate });
|
|
12535
|
+
}
|
|
12536
|
+
await assertFindingsClosed(scope, prepared, "repair-delivery-source");
|
|
12537
|
+
assertExecutionHolder(context.row, session.session_id, scope.planId, "repair-delivery-source");
|
|
12538
|
+
const { target, candidateSource } = assertLegacyRepairShape(context.snapshot, scope.planId, handoff);
|
|
12539
|
+
assertRepairBranchIdentity(context, scope, handoff, candidateSource, "repair-delivery-source");
|
|
12540
|
+
assertDeliveryPrCompatible(context.snapshot, candidateSource, target, scope.planId);
|
|
12541
|
+
assertStandaloneSourceGitProof(scope, handoff, candidateSource, "repair-delivery-source");
|
|
12542
|
+
}
|
|
12543
|
+
function repairDeliverySourceRow(context, scope, candidateSource) {
|
|
12544
|
+
const nextCoordination = {
|
|
12545
|
+
...context.coordination ?? { revision: 0 },
|
|
12546
|
+
revision: context.revision + 1
|
|
12547
|
+
};
|
|
12548
|
+
validateRowCoordinationInContext(context, nextCoordination, `plan ${scope.planId} coordination`);
|
|
12549
|
+
const currentBranch = context.snapshot.branch ?? {};
|
|
12550
|
+
return {
|
|
12551
|
+
row: { ...context.row, coordination: nextCoordination },
|
|
12552
|
+
coordination: nextCoordination,
|
|
12553
|
+
topLevel: {
|
|
12554
|
+
branch: { ...currentBranch, source: candidateSource }
|
|
12555
|
+
}
|
|
12556
|
+
};
|
|
12557
|
+
}
|
|
12558
|
+
async function mutateRepairDeliverySource(scope, session, sessionPath, request) {
|
|
12559
|
+
const result = await withRowCommit(scope, {
|
|
12560
|
+
expectedRevision: request.expectedRevision,
|
|
12561
|
+
precheck: async (context) => {
|
|
12562
|
+
assertCoordinatorBinding(session, sessionPath, context.snapshot);
|
|
12563
|
+
assertRepairDeliverySourceAdmission(context, scope);
|
|
12564
|
+
const handoff = requireAcceptedHandoffForRepair(context, scope.planId, request.handoffId);
|
|
12565
|
+
await assertRepairDeliverySourcePrecheck(context, scope, session, handoff);
|
|
12566
|
+
},
|
|
12567
|
+
mutate: (context) => {
|
|
12568
|
+
const handoff = requireAcceptedHandoffForRepair(context, scope.planId, request.handoffId);
|
|
12569
|
+
const { candidateSource } = assertLegacyRepairShape(context.snapshot, scope.planId, handoff);
|
|
12570
|
+
return repairDeliverySourceRow(context, scope, candidateSource);
|
|
12571
|
+
}
|
|
12572
|
+
});
|
|
12573
|
+
return {
|
|
12574
|
+
ok: true,
|
|
12575
|
+
operation: "repair-delivery-source",
|
|
12576
|
+
session,
|
|
12577
|
+
session_file: sessionPath,
|
|
12578
|
+
outcome: "delivery-source-repaired",
|
|
12579
|
+
view: buildView(scope.harnessRoot, scope.workflowId, scope.projectId, scope, result.snapshot, result.row, session, sessionPath)
|
|
12580
|
+
};
|
|
12581
|
+
}
|
|
12582
|
+
function completeStandaloneRow(context, scope, handoff) {
|
|
12583
|
+
const metadata = isPlainObject2(context.row.metadata) ? { ...context.row.metadata } : {};
|
|
12584
|
+
metadata.working_branch = handoff.source_branch;
|
|
12585
|
+
metadata.worktree_path = handoff.worktree_path;
|
|
12586
|
+
const nextCoordination = {
|
|
12587
|
+
...context.coordination ?? { revision: 0 },
|
|
12588
|
+
revision: context.revision + 1,
|
|
12589
|
+
handoff: { ...handoff, state: "completed", completed_at: nowIso() }
|
|
12590
|
+
};
|
|
12591
|
+
validateRowCoordinationInContext(context, nextCoordination, `plan ${scope.planId} coordination`);
|
|
12592
|
+
const nextRow = { ...context.row, status: "Done", metadata, coordination: nextCoordination };
|
|
12593
|
+
delete nextRow.execution_lease;
|
|
12594
|
+
return { row: nextRow, coordination: nextCoordination };
|
|
12595
|
+
}
|
|
12596
|
+
function assertStandaloneCompletedReplay(context, scope, handoff) {
|
|
12597
|
+
assertStandaloneRoute(context.snapshot, scope.planId, "reconcile");
|
|
12598
|
+
if (rowStatusOf(context.row) !== "Done") {
|
|
12599
|
+
throw new CoordinationError("coordination.invalid-transition", `reconcile requires ${scope.planId} to be Done for a standalone completed replay`, { plan_id: scope.planId, status: context.row.status });
|
|
12600
|
+
}
|
|
12601
|
+
if (context.row.execution_lease !== undefined) {
|
|
12602
|
+
throw new CoordinationError("coordination.invalid-transition", `reconcile requires no execution lease on ${scope.planId} for a standalone completed replay`, { plan_id: scope.planId });
|
|
12603
|
+
}
|
|
12604
|
+
if (context.snapshot.integration_merge_lease !== undefined) {
|
|
12605
|
+
throw new CoordinationError("coordination.invalid-transition", `reconcile requires no integration merge lease for a standalone completed replay of ${scope.planId}`, { plan_id: scope.planId });
|
|
12606
|
+
}
|
|
12607
|
+
assertNoIntegrationContamination(context, scope.planId, handoff, "reconcile");
|
|
12608
|
+
const storedHandoffViolations = validatePlanHandoff(handoff, `plan ${scope.planId} coordination.handoff`, "standalone-development");
|
|
12609
|
+
const storedHandoffFailure = storedHandoffViolations.find((entry) => !entry.ok);
|
|
12610
|
+
if (storedHandoffFailure !== undefined) {
|
|
12611
|
+
throw new CoordinationError(storedHandoffFailure.code, storedHandoffFailure.message, { plan_id: scope.planId });
|
|
12612
|
+
}
|
|
12613
|
+
const anchors = standaloneDeliveryAnchors(context.snapshot, scope.planId);
|
|
12614
|
+
assertStandaloneBranchIdentity(context, scope, handoff, anchors, "reconcile", false);
|
|
12615
|
+
const repository = proofRepository([handoff.worktree_path, scope.worktreePath, scope.harnessRoot]);
|
|
12616
|
+
if (repository !== undefined && !gitObjectExists(repository, handoff.source_sha)) {
|
|
12617
|
+
throw gitProof(`reconcile cannot re-verify the pinned standalone source ${handoff.source_sha} for plan ${scope.planId}`, { plan_id: scope.planId, source_sha: handoff.source_sha });
|
|
12618
|
+
}
|
|
12619
|
+
}
|
|
12620
|
+
async function assertIterationCompletionPrecheck(context, scope, session, handoff) {
|
|
12621
|
+
if (handoff.state !== "merged") {
|
|
12622
|
+
throw new CoordinationError("coordination.invalid-transition", `plan ${scope.planId} handoff is ${handoff.state} — complete requires a merged attempt`, { plan_id: scope.planId, state: handoff.state });
|
|
12623
|
+
}
|
|
12624
|
+
if (rowStatusOf(context.row) !== "InReview") {
|
|
12625
|
+
throw new CoordinationError("coordination.invalid-transition", `complete requires ${scope.planId} to still be InReview`, { plan_id: scope.planId, status: context.row.status });
|
|
12626
|
+
}
|
|
12627
|
+
const prepared = context.coordination?.prepared;
|
|
12628
|
+
if (prepared === undefined) {
|
|
12629
|
+
throw new CoordinationError("coordination.not-prepared", `plan ${scope.planId} is not prepared in this workflow`, { plan_id: scope.planId });
|
|
12630
|
+
}
|
|
12631
|
+
assertEvidenceDigests(handoff);
|
|
12632
|
+
await assertFindingsClosed(scope, prepared, "complete");
|
|
12633
|
+
assertExecutionHolder(context.row, session.session_id, scope.planId, "complete");
|
|
12634
|
+
assertMergeLease(context.snapshot, session, scope.planId, handoff);
|
|
12635
|
+
const integration = requireIntegration(handoff, scope.planId);
|
|
12636
|
+
const anchors = integrationAnchors(context.snapshot, scope.planId);
|
|
12637
|
+
const checkout = assertIntegrationCheckout(anchors, scope.planId);
|
|
12638
|
+
assertRecordedResult(anchors.worktreePath, scope.planId, integration, handoff.source_sha, checkout.head);
|
|
12639
|
+
}
|
|
11913
12640
|
function completeRow(context, scope, handoff, resultSha) {
|
|
11914
12641
|
const integration = requireIntegration(handoff, scope.planId);
|
|
11915
12642
|
const completed = { ...integration, result_sha: resultSha, verified_at: nowIso() };
|
|
@@ -11928,33 +12655,27 @@ function completeRow(context, scope, handoff, resultSha) {
|
|
|
11928
12655
|
dropTopLevel: release === undefined ? [] : ["integration_merge_lease"]
|
|
11929
12656
|
};
|
|
11930
12657
|
}
|
|
12658
|
+
var completeStandaloneMutateGapForTest;
|
|
11931
12659
|
async function mutateComplete(scope, session, sessionPath, request) {
|
|
11932
12660
|
const result = await withRowCommit(scope, {
|
|
11933
12661
|
expectedRevision: request.expectedRevision,
|
|
11934
12662
|
precheck: async (context) => {
|
|
11935
12663
|
assertCoordinatorBinding(session, sessionPath, context.snapshot);
|
|
11936
12664
|
const handoff = requireHandoff(context, scope.planId, request.handoffId);
|
|
11937
|
-
if (
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
if (rowStatusOf(context.row) !== "InReview") {
|
|
11941
|
-
throw new CoordinationError("coordination.invalid-transition", `complete requires ${scope.planId} to still be InReview`, { plan_id: scope.planId, status: context.row.status });
|
|
11942
|
-
}
|
|
11943
|
-
const prepared = context.coordination?.prepared;
|
|
11944
|
-
if (prepared === undefined) {
|
|
11945
|
-
throw new CoordinationError("coordination.not-prepared", `plan ${scope.planId} is not prepared in this workflow`, { plan_id: scope.planId });
|
|
12665
|
+
if (isStandaloneDevelopmentWorkflow(context.snapshot)) {
|
|
12666
|
+
await assertStandaloneCompletionPrecheck(context, scope, session, handoff);
|
|
12667
|
+
return;
|
|
11946
12668
|
}
|
|
11947
|
-
|
|
11948
|
-
await assertFindingsClosed(scope, prepared, "complete");
|
|
11949
|
-
assertExecutionHolder(context.row, session.session_id, scope.planId, "complete");
|
|
11950
|
-
assertMergeLease(context.snapshot, session, scope.planId, handoff);
|
|
11951
|
-
const integration = requireIntegration(handoff, scope.planId);
|
|
11952
|
-
const anchors = integrationAnchors(context.snapshot, scope.planId);
|
|
11953
|
-
const checkout = assertIntegrationCheckout(anchors, scope.planId);
|
|
11954
|
-
assertRecordedResult(anchors.worktreePath, scope.planId, integration, handoff.source_sha, checkout.head);
|
|
12669
|
+
await assertIterationCompletionPrecheck(context, scope, session, handoff);
|
|
11955
12670
|
},
|
|
11956
12671
|
mutate: (context) => {
|
|
11957
12672
|
const handoff = requireHandoff(context, scope.planId, request.handoffId);
|
|
12673
|
+
if (isStandaloneDevelopmentWorkflow(context.snapshot)) {
|
|
12674
|
+
completeStandaloneMutateGapForTest?.();
|
|
12675
|
+
const anchors = standaloneDeliveryAnchors(context.snapshot, scope.planId);
|
|
12676
|
+
assertStandaloneSourceGitProof(scope, handoff, anchors.source, "complete");
|
|
12677
|
+
return completeStandaloneRow(context, scope, handoff);
|
|
12678
|
+
}
|
|
11958
12679
|
const integration = requireIntegration(handoff, scope.planId);
|
|
11959
12680
|
const resultSha = assertRecordedResult(integration.worktree_path, scope.planId, integration, handoff.source_sha, undefined);
|
|
11960
12681
|
return completeRow(context, scope, handoff, resultSha);
|
|
@@ -11978,6 +12699,10 @@ async function classifyReconcile(context, scope, session, handoff, expectedHando
|
|
|
11978
12699
|
});
|
|
11979
12700
|
}
|
|
11980
12701
|
if (handoff.state === "completed") {
|
|
12702
|
+
if (isStandaloneDevelopmentWorkflow(context.snapshot)) {
|
|
12703
|
+
assertStandaloneCompletedReplay(context, scope, handoff);
|
|
12704
|
+
return { outcome: "already-completed", apply: () => null };
|
|
12705
|
+
}
|
|
11981
12706
|
const integration = requireIntegration(handoff, planId);
|
|
11982
12707
|
const repository = proofRepository([integration.worktree_path, handoff.worktree_path, session.harness_root]);
|
|
11983
12708
|
if (repository === undefined) {
|
|
@@ -13051,6 +13776,7 @@ export {
|
|
|
13051
13776
|
readWorkflowSnapshot,
|
|
13052
13777
|
recordWorkflowDelivery,
|
|
13053
13778
|
referenceExists,
|
|
13779
|
+
registerIterationWorkflow,
|
|
13054
13780
|
registerPlanWorkflow,
|
|
13055
13781
|
registerWorkflow,
|
|
13056
13782
|
releaseLease,
|
|
@@ -13095,6 +13821,7 @@ export {
|
|
|
13095
13821
|
techDebtRollup,
|
|
13096
13822
|
unregisterWorkflow,
|
|
13097
13823
|
validateAssignmentFields,
|
|
13824
|
+
validateAuditFindingGates,
|
|
13098
13825
|
validateAuditStatusBlocks,
|
|
13099
13826
|
validateCompassFrontmatter,
|
|
13100
13827
|
validateDesignTokenFrontmatter,
|