@zq-silk/yui 0.6.0 → 0.6.2
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/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
3
|
+
import { isSemanticReviewRound } from "./reviewOutcomeClassifier.js";
|
|
4
|
+
import { createReviewFinding, disposeReviewFinding, isReviewFindingBlocking, normalizeReviewFindingSeverity, redetectReviewFinding, resolveReviewFinding, reviewFindingStableKey, touchReviewFinding, validateReviewFinding } from "./reviewFinding.js";
|
|
5
|
+
/** Resolves the durable feature flag; absent config defaults to shadow. */
|
|
6
|
+
export function reviewFindingLedgerMode(config) {
|
|
7
|
+
const mode = config.review?.findingLedger;
|
|
8
|
+
return mode === "enforce" ? "enforce" : "shadow";
|
|
9
|
+
}
|
|
10
|
+
export const REVIEW_FINDINGS_RECONCILE_FAILED_EVENT = "review.findings-reconcile-failed";
|
|
11
|
+
/**
|
|
12
|
+
* Extracts reported findings from a completed Round. The reviewer's JSON
|
|
13
|
+
* report is the authoritative source; panel Rounds fall back to the findings
|
|
14
|
+
* attached to their ExecutionGroup lane results. A malformed or free-text
|
|
15
|
+
* report yields no findings (the raw report stays authoritative evidence).
|
|
16
|
+
*/
|
|
17
|
+
export function extractReportedFindings(round) {
|
|
18
|
+
const fromReport = extractFindingsFromReportJson(round.report ?? "");
|
|
19
|
+
if (fromReport.length > 0)
|
|
20
|
+
return fromReport;
|
|
21
|
+
return extractFindingsFromLanes(round);
|
|
22
|
+
}
|
|
23
|
+
function extractFindingsFromReportJson(report) {
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(report);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
32
|
+
return [];
|
|
33
|
+
const record = parsed;
|
|
34
|
+
if (!Array.isArray(record.findings))
|
|
35
|
+
return [];
|
|
36
|
+
return record.findings.flatMap((entry) => {
|
|
37
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
38
|
+
return [];
|
|
39
|
+
const finding = entry;
|
|
40
|
+
const summary = typeof finding.summary === "string" && finding.summary.trim().length > 0
|
|
41
|
+
? finding.summary.trim()
|
|
42
|
+
: typeof finding.title === "string" && finding.title.trim().length > 0
|
|
43
|
+
? finding.title.trim()
|
|
44
|
+
: null;
|
|
45
|
+
if (summary === null)
|
|
46
|
+
return [];
|
|
47
|
+
let severity;
|
|
48
|
+
try {
|
|
49
|
+
severity = normalizeReviewFindingSeverity(finding.severity);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const status = finding.status === "resolved" ? "resolved" : "open";
|
|
55
|
+
const title = typeof finding.title === "string" && finding.title.trim().length > 0
|
|
56
|
+
? finding.title.trim()
|
|
57
|
+
: summary;
|
|
58
|
+
const invariant = typeof finding.invariant === "string" && finding.invariant.trim().length > 0
|
|
59
|
+
? finding.invariant.trim()
|
|
60
|
+
: typeof finding.category === "string" && finding.category.trim().length > 0
|
|
61
|
+
? finding.category.trim()
|
|
62
|
+
: "review-finding";
|
|
63
|
+
return [{
|
|
64
|
+
...(typeof finding.id === "string" && finding.id.trim().length > 0
|
|
65
|
+
? { sourceId: finding.id.trim() }
|
|
66
|
+
: {}),
|
|
67
|
+
severity,
|
|
68
|
+
status,
|
|
69
|
+
invariant,
|
|
70
|
+
title,
|
|
71
|
+
affectedPaths: stringList(finding.paths ?? finding.affectedPaths),
|
|
72
|
+
affectedSymbols: stringList(finding.symbols ?? finding.affectedSymbols),
|
|
73
|
+
evidence: stringList(finding.evidence)
|
|
74
|
+
}];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function extractFindingsFromLanes(round) {
|
|
78
|
+
const lanes = round.executionGroup?.lanes ?? [];
|
|
79
|
+
return lanes.flatMap((lane) => {
|
|
80
|
+
const findings = lane.result?.findings ?? [];
|
|
81
|
+
return findings.flatMap((finding) => {
|
|
82
|
+
let severity;
|
|
83
|
+
try {
|
|
84
|
+
severity = normalizeReviewFindingSeverity(finding.severity);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
return [{
|
|
90
|
+
sourceId: finding.id,
|
|
91
|
+
severity,
|
|
92
|
+
status: finding.status === "resolved" ? "resolved" : "open",
|
|
93
|
+
invariant: "review-finding",
|
|
94
|
+
title: finding.summary,
|
|
95
|
+
affectedPaths: [],
|
|
96
|
+
affectedSymbols: [],
|
|
97
|
+
evidence: []
|
|
98
|
+
}];
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function stringList(value) {
|
|
103
|
+
if (!Array.isArray(value))
|
|
104
|
+
return [];
|
|
105
|
+
return value
|
|
106
|
+
.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
107
|
+
.map((entry) => entry.trim());
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Reconciles one completed Round's findings into the Task ledger. Stable-key
|
|
111
|
+
* matches update the existing record; ambiguous matches (multiple records with
|
|
112
|
+
* the same key) create a `mergeRequired` record instead of silently merging.
|
|
113
|
+
* Infra (failed) Rounds are skipped without touching the ledger.
|
|
114
|
+
*/
|
|
115
|
+
export function reconcileReviewFindings(store, taskId, roundId, now) {
|
|
116
|
+
const round = store.getReviewRound(taskId, roundId);
|
|
117
|
+
if (round === null) {
|
|
118
|
+
return { roundId, skipped: true, reason: "ReviewRound not found.", created: [], updated: [], conflicts: [] };
|
|
119
|
+
}
|
|
120
|
+
if (!isSemanticReviewRound(round)) {
|
|
121
|
+
return {
|
|
122
|
+
roundId,
|
|
123
|
+
skipped: true,
|
|
124
|
+
reason: "ReviewRound is an execution-attempt failure, not a semantic report.",
|
|
125
|
+
created: [],
|
|
126
|
+
updated: [],
|
|
127
|
+
conflicts: []
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const extracted = extractReportedFindings(round);
|
|
131
|
+
if (extracted.length === 0) {
|
|
132
|
+
return { roundId, skipped: true, reason: "ReviewRound report carries no findings.", created: [], updated: [], conflicts: [] };
|
|
133
|
+
}
|
|
134
|
+
const ledger = store.listReviewFindings(taskId);
|
|
135
|
+
const created = [];
|
|
136
|
+
const updated = [];
|
|
137
|
+
const conflicts = [];
|
|
138
|
+
for (const entry of extracted) {
|
|
139
|
+
const stableKey = reviewFindingStableKey({
|
|
140
|
+
invariant: entry.invariant,
|
|
141
|
+
primaryPath: entry.affectedPaths[0],
|
|
142
|
+
primarySymbol: entry.affectedSymbols[0],
|
|
143
|
+
title: entry.title
|
|
144
|
+
});
|
|
145
|
+
const matches = ledger.filter((finding) => finding.stableKey === stableKey);
|
|
146
|
+
if (matches.length === 1) {
|
|
147
|
+
const existing = matches[0];
|
|
148
|
+
const next = entry.status === "resolved"
|
|
149
|
+
? resolveReviewFinding(existing, {
|
|
150
|
+
reviewRoundId: round.id,
|
|
151
|
+
evidence: entry.evidence,
|
|
152
|
+
now
|
|
153
|
+
})
|
|
154
|
+
: existing.disposition === "accepted-risk"
|
|
155
|
+
|| existing.disposition === "not-actionable"
|
|
156
|
+
|| existing.disposition === "superseded"
|
|
157
|
+
? touchReviewFinding(existing, {
|
|
158
|
+
reviewRoundId: round.id,
|
|
159
|
+
evidence: entry.evidence,
|
|
160
|
+
now
|
|
161
|
+
})
|
|
162
|
+
: redetectReviewFinding(existing, {
|
|
163
|
+
reviewRoundId: round.id,
|
|
164
|
+
evidence: entry.evidence,
|
|
165
|
+
now
|
|
166
|
+
});
|
|
167
|
+
if (next !== existing) {
|
|
168
|
+
store.saveReviewFinding(taskId, next);
|
|
169
|
+
updated.push(next);
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (matches.length > 1) {
|
|
174
|
+
// Stable-key collision: keep every evidence trail and require an explicit
|
|
175
|
+
// Leader merge instead of silently overwriting either record.
|
|
176
|
+
const existingConflict = matches.find((finding) => finding.mergeRequired === true);
|
|
177
|
+
if (existingConflict !== undefined) {
|
|
178
|
+
// A merge-required record already exists for this stable key; refresh
|
|
179
|
+
// its evidence and last-Round pointer instead of creating unbounded
|
|
180
|
+
// duplicate conflict records.
|
|
181
|
+
const refreshed = touchReviewFinding(existingConflict, {
|
|
182
|
+
reviewRoundId: round.id,
|
|
183
|
+
evidence: entry.evidence,
|
|
184
|
+
now
|
|
185
|
+
});
|
|
186
|
+
if (refreshed !== existingConflict) {
|
|
187
|
+
store.saveReviewFinding(taskId, refreshed);
|
|
188
|
+
updated.push(refreshed);
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const conflict = createReviewFinding(store.nextReviewFindingId(taskId), taskId, {
|
|
193
|
+
...(entry.sourceId === undefined ? {} : { sourceId: entry.sourceId }),
|
|
194
|
+
stableKey,
|
|
195
|
+
severity: entry.severity,
|
|
196
|
+
invariant: entry.invariant,
|
|
197
|
+
title: entry.title,
|
|
198
|
+
affectedPaths: entry.affectedPaths,
|
|
199
|
+
affectedSymbols: entry.affectedSymbols,
|
|
200
|
+
evidence: entry.evidence,
|
|
201
|
+
reviewRoundId: round.id
|
|
202
|
+
}, now);
|
|
203
|
+
const flagged = validateReviewFinding({ ...conflict, mergeRequired: true });
|
|
204
|
+
store.saveReviewFinding(taskId, flagged);
|
|
205
|
+
conflicts.push(flagged);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const createdFinding = createReviewFinding(store.nextReviewFindingId(taskId), taskId, {
|
|
209
|
+
...(entry.sourceId === undefined ? {} : { sourceId: entry.sourceId }),
|
|
210
|
+
stableKey,
|
|
211
|
+
severity: entry.severity,
|
|
212
|
+
invariant: entry.invariant,
|
|
213
|
+
title: entry.title,
|
|
214
|
+
affectedPaths: entry.affectedPaths,
|
|
215
|
+
affectedSymbols: entry.affectedSymbols,
|
|
216
|
+
evidence: entry.evidence,
|
|
217
|
+
reviewRoundId: round.id
|
|
218
|
+
}, now);
|
|
219
|
+
store.saveReviewFinding(taskId, createdFinding);
|
|
220
|
+
created.push(createdFinding);
|
|
221
|
+
}
|
|
222
|
+
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "review.findings-reconciled", {
|
|
223
|
+
reviewRoundId: round.id,
|
|
224
|
+
created: String(created.length),
|
|
225
|
+
updated: String(updated.length),
|
|
226
|
+
conflicts: String(conflicts.length)
|
|
227
|
+
}, now));
|
|
228
|
+
return { roundId, skipped: false, created, updated, conflicts };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Reconciles a delivered Review without making ledger availability a
|
|
232
|
+
* precondition for preserving the reviewer's free-form report. A ledger
|
|
233
|
+
* read/write failure is recorded as a Task event; `enforce` completion fails
|
|
234
|
+
* closed until the Leader recovers the ledger.
|
|
235
|
+
*/
|
|
236
|
+
export function reconcileReviewFindingsAfterReview(store, taskId, roundId, now) {
|
|
237
|
+
try {
|
|
238
|
+
return reconcileReviewFindings(store, taskId, roundId, now);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
const reason = `Review finding ledger is unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
242
|
+
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, REVIEW_FINDINGS_RECONCILE_FAILED_EVENT, { reviewRoundId: roundId, reason }, now));
|
|
243
|
+
return {
|
|
244
|
+
roundId,
|
|
245
|
+
skipped: true,
|
|
246
|
+
reason,
|
|
247
|
+
created: [],
|
|
248
|
+
updated: [],
|
|
249
|
+
conflicts: []
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** True when a semantic Review could not be reconciled into the ledger. */
|
|
254
|
+
export function reviewFindingLedgerWriteFailed(store, taskId) {
|
|
255
|
+
const latestByRound = new Map();
|
|
256
|
+
for (const event of store.listEvents(taskId)) {
|
|
257
|
+
if (event.type !== REVIEW_FINDINGS_RECONCILE_FAILED_EVENT
|
|
258
|
+
&& event.type !== "review.findings-reconciled") {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const reviewRoundId = event.payload.reviewRoundId;
|
|
262
|
+
if (reviewRoundId !== undefined)
|
|
263
|
+
latestByRound.set(reviewRoundId, event);
|
|
264
|
+
}
|
|
265
|
+
return [...latestByRound.values()]
|
|
266
|
+
.some((event) => event.type === REVIEW_FINDINGS_RECONCILE_FAILED_EVENT);
|
|
267
|
+
}
|
|
268
|
+
/** Applies one explicit Leader disposition to a finding. */
|
|
269
|
+
export function dispositionReviewFinding(store, taskId, findingId, command) {
|
|
270
|
+
const existing = store.getReviewFinding(taskId, findingId);
|
|
271
|
+
if (existing === null) {
|
|
272
|
+
throw new Error(`ReviewFinding not found: ${taskId}/${findingId}.`);
|
|
273
|
+
}
|
|
274
|
+
const next = disposeReviewFinding(existing, {
|
|
275
|
+
disposition: command.disposition,
|
|
276
|
+
by: command.by,
|
|
277
|
+
...(command.note === undefined ? {} : { note: command.note }),
|
|
278
|
+
...(command.workItemId === undefined
|
|
279
|
+
&& command.commit === undefined
|
|
280
|
+
&& command.verification === undefined
|
|
281
|
+
? {}
|
|
282
|
+
: {
|
|
283
|
+
repair: {
|
|
284
|
+
...(command.workItemId === undefined ? {} : { workItemId: command.workItemId }),
|
|
285
|
+
...(command.commit === undefined ? {} : { commit: command.commit }),
|
|
286
|
+
...(command.verification === undefined ? {} : { verification: command.verification })
|
|
287
|
+
}
|
|
288
|
+
}),
|
|
289
|
+
...(command.supersededBy === undefined ? {} : { supersededBy: command.supersededBy }),
|
|
290
|
+
now: command.now
|
|
291
|
+
});
|
|
292
|
+
store.saveReviewFinding(taskId, next);
|
|
293
|
+
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "review.finding-dispositioned", {
|
|
294
|
+
reviewFindingId: next.id,
|
|
295
|
+
disposition: next.disposition,
|
|
296
|
+
by: command.by
|
|
297
|
+
}, command.now));
|
|
298
|
+
return next;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Groups open P1/P2 findings into repair waves by file/symbol/invariant
|
|
302
|
+
* overlap. Findings sharing an affected path, symbol, or invariant land in
|
|
303
|
+
* the same group (union-find), so one WorkItem repairs one overlapping set
|
|
304
|
+
* while disjoint groups can run in parallel.
|
|
305
|
+
*/
|
|
306
|
+
export function planRepairGroups(store, taskId) {
|
|
307
|
+
const open = store.listReviewFindings(taskId)
|
|
308
|
+
.filter((finding) => finding.disposition === "open"
|
|
309
|
+
&& (finding.severity === "p1" || finding.severity === "p2"))
|
|
310
|
+
.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
|
|
311
|
+
const parent = new Map(open.map((finding) => [finding.id, finding.id]));
|
|
312
|
+
const root = (id) => {
|
|
313
|
+
let current = id;
|
|
314
|
+
while (parent.get(current) !== current) {
|
|
315
|
+
current = parent.get(current);
|
|
316
|
+
}
|
|
317
|
+
let cursor = id;
|
|
318
|
+
while (parent.get(cursor) !== cursor) {
|
|
319
|
+
const next = parent.get(cursor);
|
|
320
|
+
parent.set(cursor, current);
|
|
321
|
+
cursor = next;
|
|
322
|
+
}
|
|
323
|
+
return current;
|
|
324
|
+
};
|
|
325
|
+
const union = (left, right) => {
|
|
326
|
+
const leftRoot = root(left);
|
|
327
|
+
const rightRoot = root(right);
|
|
328
|
+
if (leftRoot !== rightRoot)
|
|
329
|
+
parent.set(rightRoot, leftRoot);
|
|
330
|
+
};
|
|
331
|
+
const byPath = new Map();
|
|
332
|
+
const bySymbol = new Map();
|
|
333
|
+
const byInvariant = new Map();
|
|
334
|
+
for (const finding of open) {
|
|
335
|
+
for (const path of finding.affectedPaths) {
|
|
336
|
+
const seen = byPath.get(path);
|
|
337
|
+
if (seen === undefined)
|
|
338
|
+
byPath.set(path, finding.id);
|
|
339
|
+
else
|
|
340
|
+
union(seen, finding.id);
|
|
341
|
+
}
|
|
342
|
+
for (const symbol of finding.affectedSymbols) {
|
|
343
|
+
const seen = bySymbol.get(symbol);
|
|
344
|
+
if (seen === undefined)
|
|
345
|
+
bySymbol.set(symbol, finding.id);
|
|
346
|
+
else
|
|
347
|
+
union(seen, finding.id);
|
|
348
|
+
}
|
|
349
|
+
// Issue 06: only union on a real invariant. Findings that lack an
|
|
350
|
+
// explicit invariant/category all share the fallback "review-finding"
|
|
351
|
+
// label; unioning on it would collapse sparse reports into one group and
|
|
352
|
+
// defeat parallel repair.
|
|
353
|
+
if (finding.invariant !== "review-finding") {
|
|
354
|
+
const seen = byInvariant.get(finding.invariant);
|
|
355
|
+
if (seen === undefined)
|
|
356
|
+
byInvariant.set(finding.invariant, finding.id);
|
|
357
|
+
else
|
|
358
|
+
union(seen, finding.id);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const groups = new Map();
|
|
362
|
+
for (const finding of open) {
|
|
363
|
+
const key = root(finding.id);
|
|
364
|
+
const members = groups.get(key) ?? [];
|
|
365
|
+
members.push(finding);
|
|
366
|
+
groups.set(key, members);
|
|
367
|
+
}
|
|
368
|
+
return [...groups.values()]
|
|
369
|
+
.map((findings) => ({
|
|
370
|
+
groupKey: findings.map(({ id }) => id).join("+"),
|
|
371
|
+
findings,
|
|
372
|
+
findingIds: findings.map(({ id }) => id),
|
|
373
|
+
affectedPaths: [...new Set(findings.flatMap(({ affectedPaths }) => affectedPaths))].sort(),
|
|
374
|
+
affectedSymbols: [...new Set(findings.flatMap(({ affectedSymbols }) => affectedSymbols))].sort(),
|
|
375
|
+
invariants: [...new Set(findings.map(({ invariant }) => invariant))].sort()
|
|
376
|
+
}))
|
|
377
|
+
.sort((left, right) => left.groupKey.localeCompare(right.groupKey, undefined, { numeric: true }));
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Completion gate: open (or repair-pending) P1/P2 findings block Task
|
|
381
|
+
* completion under `enforce`. P3/backlog findings never block.
|
|
382
|
+
*/
|
|
383
|
+
export function blockingOpenFindings(store, taskId) {
|
|
384
|
+
return store.listReviewFindings(taskId)
|
|
385
|
+
.filter(isReviewFindingBlocking)
|
|
386
|
+
.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
|
|
387
|
+
}
|
|
388
|
+
/** True when the completion gate must fail closed for this Task. */
|
|
389
|
+
export function completionGateBlocked(store, taskId) {
|
|
390
|
+
if (reviewFindingLedgerMode(store.getConfig()) !== "enforce")
|
|
391
|
+
return false;
|
|
392
|
+
return reviewFindingLedgerWriteFailed(store, taskId)
|
|
393
|
+
|| blockingOpenFindings(store, taskId).length > 0;
|
|
394
|
+
}
|
|
395
|
+
export function summarizeFindingLedger(store, taskId) {
|
|
396
|
+
const findings = store.listReviewFindings(taskId)
|
|
397
|
+
.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
|
|
398
|
+
const byDisposition = (disposition) => findings.filter((finding) => finding.disposition === disposition);
|
|
399
|
+
return {
|
|
400
|
+
total: findings.length,
|
|
401
|
+
open: byDisposition("open"),
|
|
402
|
+
fixedPendingReview: byDisposition("fixed-pending-review"),
|
|
403
|
+
verifiedFixed: byDisposition("verified-fixed"),
|
|
404
|
+
acceptedRisk: byDisposition("accepted-risk"),
|
|
405
|
+
notActionable: byDisposition("not-actionable"),
|
|
406
|
+
superseded: byDisposition("superseded"),
|
|
407
|
+
blocking: findings.filter(isReviewFindingBlocking),
|
|
408
|
+
mergeRequired: findings.filter((finding) => finding.mergeRequired === true)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Renders the ledger summary as the incremental Review context block: it
|
|
413
|
+
* replaces repeated full-history restatement while still listing every
|
|
414
|
+
* disposition class the final report must account for.
|
|
415
|
+
*/
|
|
416
|
+
export function renderFindingLedgerContext(summary) {
|
|
417
|
+
const lines = ["Finding ledger:"];
|
|
418
|
+
lines.push(`- total: ${summary.total}`);
|
|
419
|
+
const renderList = (label, findings) => {
|
|
420
|
+
if (findings.length === 0)
|
|
421
|
+
return;
|
|
422
|
+
lines.push(`- ${label}:`);
|
|
423
|
+
for (const finding of findings) {
|
|
424
|
+
lines.push(` - ${finding.id} [${finding.severity}] ${finding.title}`
|
|
425
|
+
+ ` (invariant: ${finding.invariant}; first: ${finding.firstReviewRoundId}; last: ${finding.lastReviewRoundId})`);
|
|
426
|
+
if (finding.repair !== undefined) {
|
|
427
|
+
const repairParts = [
|
|
428
|
+
finding.repair.workItemId === undefined ? "" : `work-item=${finding.repair.workItemId}`,
|
|
429
|
+
finding.repair.commit === undefined ? "" : `commit=${finding.repair.commit}`,
|
|
430
|
+
finding.repair.verification === undefined ? "" : `verification=${finding.repair.verification}`
|
|
431
|
+
].filter((part) => part.length > 0);
|
|
432
|
+
if (repairParts.length > 0)
|
|
433
|
+
lines.push(` repair: ${repairParts.join("; ")}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
renderList("verified-fixed", summary.verifiedFixed);
|
|
438
|
+
renderList("open (new/repair pending)", summary.open);
|
|
439
|
+
renderList("fixed-pending-review", summary.fixedPendingReview);
|
|
440
|
+
renderList("accepted-risk", summary.acceptedRisk);
|
|
441
|
+
renderList("not-actionable (backlog)", summary.notActionable);
|
|
442
|
+
renderList("superseded", summary.superseded);
|
|
443
|
+
if (summary.mergeRequired.length > 0) {
|
|
444
|
+
lines.push(`- merge-required: ${summary.mergeRequired.map(({ id }) => id).join(", ")}`);
|
|
445
|
+
}
|
|
446
|
+
if (summary.blocking.length > 0) {
|
|
447
|
+
lines.push(`- residual blocking P1/P2: ${summary.blocking.map(({ id }) => id).join(", ")}`);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
lines.push("- residual blocking P1/P2: none");
|
|
451
|
+
}
|
|
452
|
+
return lines.join("\n");
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Fallback gate-evidence reuse (Issue 08 artifacts are optional): a completed
|
|
456
|
+
* Task-final Round whose evidence commit exactly matches the candidate's
|
|
457
|
+
* primary head may donate its checks by digest. A changed head returns null,
|
|
458
|
+
* so an old GREEN can never be reused against a different tree.
|
|
459
|
+
*/
|
|
460
|
+
export function reusableTaskReviewEvidence(store, taskId, candidate) {
|
|
461
|
+
const rounds = store.listReviewRounds(taskId)
|
|
462
|
+
.filter((round) => (round.scope ?? "work-item") === "task"
|
|
463
|
+
&& round.status === "completed"
|
|
464
|
+
&& round.evidenceCommit !== undefined
|
|
465
|
+
&& isSameTaskReviewCandidate(round.taskCandidate, candidate))
|
|
466
|
+
.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
|
|
467
|
+
const latest = rounds.at(-1);
|
|
468
|
+
if (latest === undefined || latest.evidenceCommit === undefined)
|
|
469
|
+
return null;
|
|
470
|
+
const checks = latest.checks ?? [];
|
|
471
|
+
if (checks.length === 0 || checks.some((check) => check.outcome !== "passed"))
|
|
472
|
+
return null;
|
|
473
|
+
const digest = createHash("sha256")
|
|
474
|
+
.update(JSON.stringify(checks))
|
|
475
|
+
.digest("hex");
|
|
476
|
+
return {
|
|
477
|
+
reviewRoundId: latest.id,
|
|
478
|
+
evidenceCommit: latest.evidenceCommit,
|
|
479
|
+
digest,
|
|
480
|
+
checks
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Builds the Issue 06 incremental Review context. The Reviewer receives the
|
|
485
|
+
* cross-Round ledger, exact old/new frozen heads, repair evidence, and any
|
|
486
|
+
* reusable exact-head checks. A changed head or failed/absent checks returns
|
|
487
|
+
* no reusable evidence, so an old GREEN can never be borrowed for a new tree.
|
|
488
|
+
*/
|
|
489
|
+
export function buildTaskFinalReviewFindingContext(store, taskId, candidate) {
|
|
490
|
+
const previousSemanticRound = store.listReviewRounds(taskId)
|
|
491
|
+
.filter((round) => (round.scope ?? "work-item") === "task" && round.status === "completed")
|
|
492
|
+
.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }))
|
|
493
|
+
.at(-1) ?? null;
|
|
494
|
+
const reusableEvidence = reusableTaskReviewEvidence(store, taskId, candidate);
|
|
495
|
+
// dbonly: the finding ledger is SQLite-native. A file-only Home (no yui.db)
|
|
496
|
+
// cannot serve findings; the Review dispatch must still proceed, so the
|
|
497
|
+
// ledger context degrades to an explicit "unavailable" note.
|
|
498
|
+
let ledgerContext;
|
|
499
|
+
try {
|
|
500
|
+
ledgerContext = renderFindingLedgerContext(summarizeFindingLedger(store, taskId));
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
ledgerContext = "Finding ledger: unavailable (SQLite backend required; run `yui update` to migrate this Home).";
|
|
504
|
+
}
|
|
505
|
+
const lines = [
|
|
506
|
+
"Review convergence context:",
|
|
507
|
+
ledgerContext,
|
|
508
|
+
...(previousSemanticRound === null
|
|
509
|
+
? ["Previous semantic Task-final ReviewRound: none."]
|
|
510
|
+
: [
|
|
511
|
+
`Previous semantic Task-final ReviewRound: ${previousSemanticRound.id}`,
|
|
512
|
+
...candidate.projects.map((project) => {
|
|
513
|
+
const previous = previousSemanticRound.taskCandidate?.projects
|
|
514
|
+
.find((entry) => entry.projectId === project.projectId)?.commit;
|
|
515
|
+
return previous === undefined
|
|
516
|
+
? `Exact review boundary for ${project.projectId}: new head ${project.commit}`
|
|
517
|
+
: previous === project.commit
|
|
518
|
+
? `Exact review boundary for ${project.projectId}: unchanged at ${project.commit}`
|
|
519
|
+
: `Exact diff for ${project.projectId}: ${previous}..${project.commit}`;
|
|
520
|
+
})
|
|
521
|
+
]),
|
|
522
|
+
...(reusableEvidence === null
|
|
523
|
+
? [
|
|
524
|
+
"Reusable gate evidence: none (head changed, checks are absent, or a check did not pass); rerun the checks needed for this exact tree."
|
|
525
|
+
]
|
|
526
|
+
: [
|
|
527
|
+
`Reusable gate evidence: ${reusableEvidence.reviewRoundId}@${reusableEvidence.evidenceCommit}`,
|
|
528
|
+
`Reusable check digest: ${reusableEvidence.digest}`,
|
|
529
|
+
`Reusable checks: ${reusableEvidence.checks.map(({ name, outcome }) => `${name}=${outcome}`).join(", ")}`
|
|
530
|
+
]),
|
|
531
|
+
"Reuse each listed ledger finding id when it is still valid; explain why a new finding is not already covered by the ledger.",
|
|
532
|
+
"Your final report must clearly list verified-fixed findings, new findings, accepted risks, and residual verification gaps."
|
|
533
|
+
];
|
|
534
|
+
return {
|
|
535
|
+
context: lines.join("\n"),
|
|
536
|
+
previousSemanticRound,
|
|
537
|
+
reusableEvidence
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function isSameTaskReviewCandidate(left, right) {
|
|
541
|
+
return left !== undefined
|
|
542
|
+
&& left.projects.length === right.projects.length
|
|
543
|
+
&& left.projects.every((project, index) => (project.projectId === right.projects[index]?.projectId
|
|
544
|
+
&& project.commit === right.projects[index]?.commit));
|
|
545
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const INFRA_SIGNATURES = [
|
|
2
|
+
{
|
|
3
|
+
kind: "session-not-stopped",
|
|
4
|
+
pattern: /session must be stopped before workspace migration/iu
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
kind: "run-start",
|
|
8
|
+
pattern: /role run could not start|could not start (?:the )?(?:reviewer|role) run/iu
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
kind: "storage-lock",
|
|
12
|
+
pattern: /\.state\.lock|state lock|lock timeout|storage lock/iu
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
kind: "tmux-exit",
|
|
16
|
+
pattern: /tmux[^\n]{0,80}(?:exited|exit|died|vanished)|pane (?:exited|died)/iu
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
kind: "yield-timeout",
|
|
20
|
+
pattern: /yield timeout|controller yield timeout|yield timed out/iu
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
kind: "run-identity",
|
|
24
|
+
pattern: /wrong run id|unknown run id|run id (?:is )?(?:invalid|unknown|mismatch)/iu
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
kind: "policy",
|
|
28
|
+
pattern: /cyber_?policy|policy denial|permission denied by policy/iu
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
kind: "baseline-contamination",
|
|
32
|
+
pattern: /cross[-\s]?baseline|baseline pollution|contaminated baseline|wrong base sha/iu
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Classifies a terminal Round. `completed` is always semantic (the reviewer
|
|
37
|
+
* delivered a report); `failed` is always infra. Non-terminal Rounds have no
|
|
38
|
+
* outcome yet.
|
|
39
|
+
*/
|
|
40
|
+
export function classifyReviewRoundOutcome(round) {
|
|
41
|
+
if (round.status === "completed") {
|
|
42
|
+
return { kind: "semantic", reason: "Reviewer delivered a report." };
|
|
43
|
+
}
|
|
44
|
+
if (round.status !== "failed")
|
|
45
|
+
return null;
|
|
46
|
+
const text = `${round.summary ?? ""}\n${round.report ?? ""}`;
|
|
47
|
+
for (const { kind, pattern } of INFRA_SIGNATURES) {
|
|
48
|
+
if (pattern.test(text)) {
|
|
49
|
+
return { kind: "infra", infraKind: kind, reason: `Review execution attempt failed (${kind}).` };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
kind: "infra",
|
|
54
|
+
infraKind: "other-infra",
|
|
55
|
+
reason: "Review execution attempt failed before a semantic report."
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** True when this Round may feed the finding ledger. */
|
|
59
|
+
export function isSemanticReviewRound(round) {
|
|
60
|
+
return classifyReviewRoundOutcome(round)?.kind === "semantic";
|
|
61
|
+
}
|
|
@@ -2,7 +2,7 @@ import { requireIdentity, requireText, requireTimestamp } from "../domain/valida
|
|
|
2
2
|
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
3
|
import { validateTaskFinalReviewContract } from "./taskFinalReviewContract.js";
|
|
4
4
|
import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
5
|
-
import { assertExecutionGroupTransition, validateExecutionGroup } from "../execution/executionGroup.js";
|
|
5
|
+
import { assertExecutionGroupTransition, resetReviewExecutionLane, validateExecutionGroup } from "../execution/executionGroup.js";
|
|
6
6
|
export function createReviewRound(id, taskId, workItemId, candidateId, reviewerRoleName, requestedBy, reviewBaseCommit, now, executionGroup) {
|
|
7
7
|
return validateReviewRound({
|
|
8
8
|
schemaVersion: 4,
|
|
@@ -84,6 +84,56 @@ export function finishReviewRound(round, status, summary, now, result = {}) {
|
|
|
84
84
|
endedAt: now.toISOString()
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Issue 06: retry a failed Task-final execution attempt under the same semantic
|
|
89
|
+
* Round identity. AgentRun history remains the attempt trail; the Round itself
|
|
90
|
+
* returns to pending so infrastructure retries do not manufacture a new
|
|
91
|
+
* semantic ReviewRound or duplicate findings.
|
|
92
|
+
*/
|
|
93
|
+
export function retryTaskReviewRound(round) {
|
|
94
|
+
validateReviewRound(round);
|
|
95
|
+
if ((round.scope ?? "work-item") !== "task") {
|
|
96
|
+
throw new Error(`Only a Task-final ReviewRound can be retried in place: ${round.id}.`);
|
|
97
|
+
}
|
|
98
|
+
if (round.status !== "failed") {
|
|
99
|
+
throw new Error(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
|
|
100
|
+
}
|
|
101
|
+
const retryExecutionGroup = round.executionGroup === undefined
|
|
102
|
+
? undefined
|
|
103
|
+
: retryReviewExecutionGroup(round);
|
|
104
|
+
return validateReviewRound({
|
|
105
|
+
schemaVersion: round.schemaVersion,
|
|
106
|
+
id: round.id,
|
|
107
|
+
taskId: round.taskId,
|
|
108
|
+
workItemId: round.workItemId,
|
|
109
|
+
candidateId: round.candidateId,
|
|
110
|
+
reviewerRoleName: round.reviewerRoleName,
|
|
111
|
+
reviewBaseCommit: round.reviewBaseCommit,
|
|
112
|
+
scope: "task",
|
|
113
|
+
...(round.taskCandidate === undefined ? {} : { taskCandidate: round.taskCandidate }),
|
|
114
|
+
...(round.taskFinalReviewContract === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: { taskFinalReviewContract: round.taskFinalReviewContract }),
|
|
117
|
+
// Keep the historical attempt Group and Lane addressable from AgentRun
|
|
118
|
+
// history while resetting the Lane for another dispatch attempt.
|
|
119
|
+
...(retryExecutionGroup === undefined ? {} : { executionGroup: retryExecutionGroup }),
|
|
120
|
+
requestedBy: "leader",
|
|
121
|
+
status: "pending",
|
|
122
|
+
...(round.workspace === undefined ? {} : { workspace: round.workspace }),
|
|
123
|
+
createdAt: round.createdAt
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function retryReviewExecutionGroup(round) {
|
|
127
|
+
const previous = round.executionGroup;
|
|
128
|
+
const attemptTime = Date.parse(round.endedAt ?? round.createdAt);
|
|
129
|
+
const now = new Date(attemptTime);
|
|
130
|
+
const lanes = previous.lanes.map((lane) => (resetReviewExecutionLane(previous, lane.id, now)));
|
|
131
|
+
return validateExecutionGroup({
|
|
132
|
+
...previous,
|
|
133
|
+
lanes,
|
|
134
|
+
updatedAt: now.toISOString()
|
|
135
|
+
});
|
|
136
|
+
}
|
|
87
137
|
/** Accepts the Reviewer's complete report and extracts only optional known evidence. */
|
|
88
138
|
export function parseReviewYieldReport(value) {
|
|
89
139
|
const report = requireText(value, "Review report");
|
|
@@ -265,10 +315,12 @@ export function validateReviewRound(round) {
|
|
|
265
315
|
requireText(round.report ?? "", "Review report");
|
|
266
316
|
validateChecks(round.checks ?? []);
|
|
267
317
|
if (round.evidenceCommit !== undefined) {
|
|
318
|
+
// The exact commit on which the review's checks ran. Equals the base
|
|
319
|
+
// when the reviewer ran checks on the frozen candidate tree; differs
|
|
320
|
+
// when the reviewer committed diagnostics on top of it. A dirty
|
|
321
|
+
// review with uncommitted changes records no evidenceCommit, since no
|
|
322
|
+
// single commit captures the checked tree.
|
|
268
323
|
requireCommit(round.evidenceCommit, "Review evidence commit");
|
|
269
|
-
if (round.evidenceCommit === round.reviewBaseCommit) {
|
|
270
|
-
throw new Error("Review evidence commit must differ from its review base.");
|
|
271
|
-
}
|
|
272
324
|
}
|
|
273
325
|
requireTimestamp(round.endedAt ?? "", "ReviewRound endedAt");
|
|
274
326
|
}
|