@zq-silk/yui 0.14.1 → 0.15.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.
Files changed (185) hide show
  1. package/ARCHITECTURE.md +91 -47
  2. package/README.md +275 -145
  3. package/dist/agent/managedRuntimeEnvironment.js +5 -8
  4. package/dist/cli/commandCatalog.js +74 -109
  5. package/dist/cli/helpRenderer.js +1 -2
  6. package/dist/cli/interactionCandidates.js +10 -0
  7. package/dist/cli/interactionPolicy.js +76 -37
  8. package/dist/cli/roleWizard.js +5 -6
  9. package/dist/cli/updateCommand.js +17 -9
  10. package/dist/cli/updateOrchestrator.js +87 -19
  11. package/dist/cli/updatePorts.js +79 -13
  12. package/dist/cli/upgradeCommand.js +105 -23
  13. package/dist/cli.js +268 -231
  14. package/dist/commands/agentCommands.js +13 -0
  15. package/dist/commands/configCommands.js +7 -19
  16. package/dist/commands/configOverview.js +3 -2
  17. package/dist/commands/controllerCommands.js +1 -1
  18. package/dist/commands/durableJobCommands.js +1 -1
  19. package/dist/commands/executionAuditCommands.js +4 -9
  20. package/dist/commands/globalRoleCommands.js +17 -0
  21. package/dist/commands/profileCommands.js +200 -30
  22. package/dist/commands/sessionCommands.js +1 -1
  23. package/dist/commands/taskActor.js +29 -127
  24. package/dist/commands/taskChangeSetCommands.js +1 -3
  25. package/dist/commands/taskCommands.js +1771 -2107
  26. package/dist/commands/taskCompletionGate.js +27 -103
  27. package/dist/commands/taskContextCommand.js +47 -80
  28. package/dist/commands/taskExecutionCommands.js +9 -27
  29. package/dist/commands/taskInputCommands.js +10 -10
  30. package/dist/commands/taskIntegrationCommands.js +69 -61
  31. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  32. package/dist/commands/taskNextActionCommand.js +8 -29
  33. package/dist/commands/taskOverviewCommand.js +25 -3
  34. package/dist/commands/taskPublicationCommands.js +151 -85
  35. package/dist/commands/taskPublicationVerifyCommand.js +181 -0
  36. package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
  37. package/dist/commands/taskRoleRuntimeStatus.js +5 -6
  38. package/dist/commands/taskUpstreamCommands.js +79 -95
  39. package/dist/config/configCatalog.js +1 -1
  40. package/dist/context/contextSnapshot.js +17 -2
  41. package/dist/context/sourceTurnContext.js +30 -0
  42. package/dist/context/turnContextPack.js +107 -48
  43. package/dist/context/turnInputContract.js +11 -4
  44. package/dist/context/wakeNotification.js +84 -22
  45. package/dist/controller/agentRuntimeObserver.js +9 -12
  46. package/dist/controller/clientRuntime.js +13 -13
  47. package/dist/controller/controller.js +11 -11
  48. package/dist/controller/fileSchedulerStoreAdapter.js +262 -219
  49. package/dist/controller/jobControl.js +11 -10
  50. package/dist/controller/resourceInventory.js +6 -6
  51. package/dist/controller/resourceInventoryLinux.js +2 -2
  52. package/dist/controller/runtime.js +18 -61
  53. package/dist/controller/runtimeEventInbox.js +57 -34
  54. package/dist/controller/runtimeEventProcessor.js +36 -96
  55. package/dist/controller/runtimeHookTurnFence.js +33 -57
  56. package/dist/controller/runtimeLaunchCoordinator.js +44 -40
  57. package/dist/controller/runtimeObservationHook.js +18 -12
  58. package/dist/controller/sessionNotify.js +43 -28
  59. package/dist/controller/sessionOwnerReconciliation.js +21 -21
  60. package/dist/controller/structuredProviderObservation.js +13 -16
  61. package/dist/coordination/workMailboxQueue.js +132 -0
  62. package/dist/core/controllerServer.js +6 -5
  63. package/dist/doctor/doctor.js +37 -14
  64. package/dist/domain/agentResultTransport.js +60 -0
  65. package/dist/execution/executionHealth.js +111 -118
  66. package/dist/execution/reviewMainTurn.js +161 -0
  67. package/dist/execution/workItemExecution.js +454 -0
  68. package/dist/execution/workItemExecutionProjection.js +309 -0
  69. package/dist/execution/workItemMainTurn.js +164 -0
  70. package/dist/executor/agentExecutor.js +19 -19
  71. package/dist/executor/executorRegistry.js +7 -28
  72. package/dist/executor/fileRoleLaunchPlanner.js +16 -82
  73. package/dist/external/githubPublicationVerifier.js +114 -0
  74. package/dist/external/gitlabPublicationVerifier.js +158 -0
  75. package/dist/external/pinnedCommandRunner.js +73 -0
  76. package/dist/integration/changeSetManifest.js +0 -3
  77. package/dist/integration/deliveryObligation.js +43 -6
  78. package/dist/integration/gitIntegrationService.js +284 -101
  79. package/dist/integration/integrationAttempt.js +64 -14
  80. package/dist/integration/integrationQueueEntry.js +5 -35
  81. package/dist/integration/integrationQueueService.js +65 -194
  82. package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
  83. package/dist/lifecycle/exactTurnTerminalization.js +100 -97
  84. package/dist/message/message.js +16 -0
  85. package/dist/observability/executionAudit.js +4 -25
  86. package/dist/observability/faultClassification.js +58 -70
  87. package/dist/observability/orchestrationMetrics.js +8 -50
  88. package/dist/observability/runtimeIdentity.js +48 -50
  89. package/dist/profile/agentProfile.js +60 -15
  90. package/dist/profile/agentProfileRuntime.js +100 -0
  91. package/dist/release/releaseWorkflowPorts.js +5 -68
  92. package/dist/release/runtimeRelease.js +9 -1
  93. package/dist/repository/executionLaneGitSnapshot.js +80 -0
  94. package/dist/repository/gitWorkspace.js +9 -67
  95. package/dist/repository/taskBaseFreshness.js +3 -1
  96. package/dist/repository/taskWorkspaceCoordinator.js +32 -4
  97. package/dist/repository/taskWorkspacePreparer.js +252 -411
  98. package/dist/resources/liveReferences.js +6 -55
  99. package/dist/resources/resourceDiscovery.js +1 -30
  100. package/dist/resources/resourceRegistrar.js +0 -23
  101. package/dist/review/deltaRecheck.js +8 -65
  102. package/dist/review/reviewAcceptance.js +22 -19
  103. package/dist/review/reviewConfig.js +0 -10
  104. package/dist/review/reviewDecision.js +2 -2
  105. package/dist/review/reviewRound.js +147 -273
  106. package/dist/review/reviewerAvailability.js +13 -23
  107. package/dist/role/role.js +3 -0
  108. package/dist/runtime/agentDriverObservation.js +1 -1
  109. package/dist/runtime/agentHost.js +51 -51
  110. package/dist/runtime/builtinAgentDrivers.js +9 -2
  111. package/dist/runtime/codexAppServerRuntime.js +23 -5
  112. package/dist/runtime/exactControlPlane.js +22 -296
  113. package/dist/runtime/firstProgressAdvisory.js +1 -1
  114. package/dist/runtime/index.js +1 -1
  115. package/dist/runtime/launchBroker.js +11 -11
  116. package/dist/runtime/lifecycleReservation.js +2 -2
  117. package/dist/runtime/managedCaller.js +111 -0
  118. package/dist/runtime/ports.js +16 -3
  119. package/dist/runtime/preallocatedNativeSession.js +2 -2
  120. package/dist/runtime/processExitObservation.js +3 -2
  121. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  122. package/dist/runtime/runtimeBinding.js +1 -1
  123. package/dist/runtime/runtimeObservation.js +33 -16
  124. package/dist/runtime/runtimeSessionCandidate.js +2 -2
  125. package/dist/runtime/runtimeStopReceipt.js +11 -11
  126. package/dist/runtime/sessionLaunchRequest.js +2 -2
  127. package/dist/runtime/sessionOwnerIdentity.js +9 -9
  128. package/dist/runtime/sessionOwnerRegistry.js +13 -13
  129. package/dist/runtime/sessionReconciliation.js +4 -4
  130. package/dist/runtime/sessionTerminationGuard.js +9 -9
  131. package/dist/runtime/sessionTokenMetrics.js +5 -7
  132. package/dist/runtime/structuredProviderHost.js +5 -5
  133. package/dist/runtime/taskRuntimeIsolation.js +24 -24
  134. package/dist/runtime/tmuxAdapters.js +32 -30
  135. package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
  136. package/dist/scheduler/leaderWakeupProcessor.js +78 -55
  137. package/dist/scheduler/roleTurnLiveness.js +6 -6
  138. package/dist/scheduler/roleTurnStall.js +8 -9
  139. package/dist/scheduler/taskExecutionProjection.js +19 -41
  140. package/dist/scheduler/taskObservabilityProjection.js +75 -122
  141. package/dist/storage/currentTaskStore.js +6 -4
  142. package/dist/storage/sqliteSchema.js +135 -80
  143. package/dist/storage/sqliteStore.js +69 -52
  144. package/dist/storage/storageSchema.js +92 -230
  145. package/dist/storage/storageVersions.js +12 -13
  146. package/dist/storage/taskStore.js +8 -8
  147. package/dist/storage/upgrade/upgradeOrchestrator.js +276 -85
  148. package/dist/task/completionReadiness.js +17 -98
  149. package/dist/task/deliveryGuard.js +9 -11
  150. package/dist/task/draftPlan.js +142 -0
  151. package/dist/task/nextAction.js +179 -477
  152. package/dist/task/publicationVerification.js +1 -0
  153. package/dist/task/remoteDelivery.js +233 -0
  154. package/dist/task/task.js +108 -4
  155. package/dist/task/taskRecordReference.js +0 -1
  156. package/dist/turn/turn.js +109 -16
  157. package/dist/verification/verificationGateService.js +0 -26
  158. package/dist/version.js +3 -3
  159. package/dist/web/assets/client/components.js +99 -121
  160. package/dist/web/assets/client/i18n.js +136 -58
  161. package/dist/web/assets/client/view.js +44 -0
  162. package/dist/web/assets/styles/cards.js +3 -15
  163. package/dist/web/assets/styles/widgets.js +4 -0
  164. package/dist/web/webSnapshot.js +29 -14
  165. package/dist/workItem/dependencyGate.js +34 -0
  166. package/dist/workItem/workItem.js +99 -266
  167. package/dist/workspace/workItemChangeSetManager.js +15 -59
  168. package/docs/task-local-identity.md +9 -9
  169. package/i18n/README.zh-CN.md +189 -74
  170. package/package.json +1 -1
  171. package/skills/yui-leader/SKILL.md +272 -782
  172. package/skills/yui-operator/SKILL.md +190 -365
  173. package/skills/yui-reviewer/SKILL.md +40 -33
  174. package/skills/yui-runtime/SKILL.md +22 -0
  175. package/skills/yui-worker/SKILL.md +19 -6
  176. package/dist/execution/candidateConvergence.js +0 -623
  177. package/dist/execution/executionGroup.js +0 -854
  178. package/dist/execution/resourceBroker.js +0 -426
  179. package/dist/executor/launchPlan.js +0 -16
  180. package/dist/review/reviewFinding.js +0 -240
  181. package/dist/review/reviewFindingLedger.js +0 -550
  182. package/dist/review/reviewOutcomeClassifier.js +0 -255
  183. package/dist/scheduler/resourceQueueProjection.js +0 -71
  184. package/dist/storage/upgrade/recordVersions.js +0 -82
  185. package/dist/task/repairWave.js +0 -150
@@ -1,550 +0,0 @@
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, store)) {
121
- return {
122
- roundId,
123
- skipped: true,
124
- reason: "ReviewRound is non-semantic or ambiguous, not a proven 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
- return reviewFindingLedgerWriteFailedFromEvents(store.listEvents(taskId));
256
- }
257
- /** Pure event-fold variant of {@link reviewFindingLedgerWriteFailed}. */
258
- export function reviewFindingLedgerWriteFailedFromEvents(events) {
259
- const latestByRound = new Map();
260
- for (const event of events) {
261
- if (event.type !== REVIEW_FINDINGS_RECONCILE_FAILED_EVENT
262
- && event.type !== "review.findings-reconciled") {
263
- continue;
264
- }
265
- const reviewRoundId = event.payload.reviewRoundId;
266
- if (reviewRoundId !== undefined)
267
- latestByRound.set(reviewRoundId, event);
268
- }
269
- return [...latestByRound.values()]
270
- .some((event) => event.type === REVIEW_FINDINGS_RECONCILE_FAILED_EVENT);
271
- }
272
- /** Applies one explicit Leader disposition to a finding. */
273
- export function dispositionReviewFinding(store, taskId, findingId, command) {
274
- const existing = store.getReviewFinding(taskId, findingId);
275
- if (existing === null) {
276
- throw new Error(`ReviewFinding not found: ${taskId}/${findingId}.`);
277
- }
278
- const next = disposeReviewFinding(existing, {
279
- disposition: command.disposition,
280
- by: command.by,
281
- ...(command.note === undefined ? {} : { note: command.note }),
282
- ...(command.workItemId === undefined
283
- && command.commit === undefined
284
- && command.verification === undefined
285
- ? {}
286
- : {
287
- repair: {
288
- ...(command.workItemId === undefined ? {} : { workItemId: command.workItemId }),
289
- ...(command.commit === undefined ? {} : { commit: command.commit }),
290
- ...(command.verification === undefined ? {} : { verification: command.verification })
291
- }
292
- }),
293
- ...(command.supersededBy === undefined ? {} : { supersededBy: command.supersededBy }),
294
- now: command.now
295
- });
296
- store.saveReviewFinding(taskId, next);
297
- store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "review.finding-dispositioned", {
298
- reviewFindingId: next.id,
299
- disposition: next.disposition,
300
- by: command.by
301
- }, command.now));
302
- return next;
303
- }
304
- /**
305
- * Groups open P1/P2 findings into repair waves by file/symbol/invariant
306
- * overlap. Findings sharing an affected path, symbol, or invariant land in
307
- * the same group (union-find), so one WorkItem repairs one overlapping set
308
- * while disjoint groups can run in parallel.
309
- */
310
- export function planRepairGroups(store, taskId) {
311
- const open = store.listReviewFindings(taskId)
312
- .filter((finding) => finding.disposition === "open"
313
- && (finding.severity === "p1" || finding.severity === "p2"))
314
- .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
315
- const parent = new Map(open.map((finding) => [finding.id, finding.id]));
316
- const root = (id) => {
317
- let current = id;
318
- while (parent.get(current) !== current) {
319
- current = parent.get(current);
320
- }
321
- let cursor = id;
322
- while (parent.get(cursor) !== cursor) {
323
- const next = parent.get(cursor);
324
- parent.set(cursor, current);
325
- cursor = next;
326
- }
327
- return current;
328
- };
329
- const union = (left, right) => {
330
- const leftRoot = root(left);
331
- const rightRoot = root(right);
332
- if (leftRoot !== rightRoot)
333
- parent.set(rightRoot, leftRoot);
334
- };
335
- const byPath = new Map();
336
- const bySymbol = new Map();
337
- const byInvariant = new Map();
338
- for (const finding of open) {
339
- for (const path of finding.affectedPaths) {
340
- const seen = byPath.get(path);
341
- if (seen === undefined)
342
- byPath.set(path, finding.id);
343
- else
344
- union(seen, finding.id);
345
- }
346
- for (const symbol of finding.affectedSymbols) {
347
- const seen = bySymbol.get(symbol);
348
- if (seen === undefined)
349
- bySymbol.set(symbol, finding.id);
350
- else
351
- union(seen, finding.id);
352
- }
353
- // Issue 06: only union on a real invariant. Findings that lack an
354
- // explicit invariant/category all share the fallback "review-finding"
355
- // label; unioning on it would collapse sparse reports into one group and
356
- // defeat parallel repair.
357
- if (finding.invariant !== "review-finding") {
358
- const seen = byInvariant.get(finding.invariant);
359
- if (seen === undefined)
360
- byInvariant.set(finding.invariant, finding.id);
361
- else
362
- union(seen, finding.id);
363
- }
364
- }
365
- const groups = new Map();
366
- for (const finding of open) {
367
- const key = root(finding.id);
368
- const members = groups.get(key) ?? [];
369
- members.push(finding);
370
- groups.set(key, members);
371
- }
372
- return [...groups.values()]
373
- .map((findings) => ({
374
- groupKey: findings.map(({ id }) => id).join("+"),
375
- findings,
376
- findingIds: findings.map(({ id }) => id),
377
- affectedPaths: [...new Set(findings.flatMap(({ affectedPaths }) => affectedPaths))].sort(),
378
- affectedSymbols: [...new Set(findings.flatMap(({ affectedSymbols }) => affectedSymbols))].sort(),
379
- invariants: [...new Set(findings.map(({ invariant }) => invariant))].sort()
380
- }))
381
- .sort((left, right) => left.groupKey.localeCompare(right.groupKey, undefined, { numeric: true }));
382
- }
383
- /**
384
- * Completion gate: open (or repair-pending) P1/P2 findings block Task
385
- * completion under `enforce`. P3/backlog findings never block.
386
- */
387
- export function blockingOpenFindings(store, taskId) {
388
- return store.listReviewFindings(taskId)
389
- .filter(isReviewFindingBlocking)
390
- .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
391
- }
392
- /** True when the completion gate must fail closed for this Task. */
393
- export function completionGateBlocked(store, taskId) {
394
- if (reviewFindingLedgerMode(store.getConfig()) !== "enforce")
395
- return false;
396
- return reviewFindingLedgerWriteFailed(store, taskId)
397
- || blockingOpenFindings(store, taskId).length > 0;
398
- }
399
- export function summarizeFindingLedger(store, taskId) {
400
- const findings = store.listReviewFindings(taskId)
401
- .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
402
- const byDisposition = (disposition) => findings.filter((finding) => finding.disposition === disposition);
403
- return {
404
- total: findings.length,
405
- open: byDisposition("open"),
406
- fixedPendingReview: byDisposition("fixed-pending-review"),
407
- verifiedFixed: byDisposition("verified-fixed"),
408
- acceptedRisk: byDisposition("accepted-risk"),
409
- notActionable: byDisposition("not-actionable"),
410
- superseded: byDisposition("superseded"),
411
- blocking: findings.filter(isReviewFindingBlocking),
412
- mergeRequired: findings.filter((finding) => finding.mergeRequired === true)
413
- };
414
- }
415
- /**
416
- * Renders the ledger summary as the incremental Review context block: it
417
- * replaces repeated full-history restatement while still listing every
418
- * disposition class the final report must account for.
419
- */
420
- export function renderFindingLedgerContext(summary) {
421
- const lines = ["Finding ledger:"];
422
- lines.push(`- total: ${summary.total}`);
423
- const renderList = (label, findings) => {
424
- if (findings.length === 0)
425
- return;
426
- lines.push(`- ${label}:`);
427
- for (const finding of findings) {
428
- lines.push(` - ${finding.id} [${finding.severity}] ${finding.title}`
429
- + ` (invariant: ${finding.invariant}; first: ${finding.firstReviewRoundId}; last: ${finding.lastReviewRoundId})`);
430
- if (finding.repair !== undefined) {
431
- const repairParts = [
432
- finding.repair.workItemId === undefined ? "" : `work-item=${finding.repair.workItemId}`,
433
- finding.repair.commit === undefined ? "" : `commit=${finding.repair.commit}`,
434
- finding.repair.verification === undefined ? "" : `verification=${finding.repair.verification}`
435
- ].filter((part) => part.length > 0);
436
- if (repairParts.length > 0)
437
- lines.push(` repair: ${repairParts.join("; ")}`);
438
- }
439
- }
440
- };
441
- renderList("verified-fixed", summary.verifiedFixed);
442
- renderList("open (new/repair pending)", summary.open);
443
- renderList("fixed-pending-review", summary.fixedPendingReview);
444
- renderList("accepted-risk", summary.acceptedRisk);
445
- renderList("not-actionable (backlog)", summary.notActionable);
446
- renderList("superseded", summary.superseded);
447
- if (summary.mergeRequired.length > 0) {
448
- lines.push(`- merge-required: ${summary.mergeRequired.map(({ id }) => id).join(", ")}`);
449
- }
450
- if (summary.blocking.length > 0) {
451
- lines.push(`- residual blocking P1/P2: ${summary.blocking.map(({ id }) => id).join(", ")}`);
452
- }
453
- else {
454
- lines.push("- residual blocking P1/P2: none");
455
- }
456
- return lines.join("\n");
457
- }
458
- /**
459
- * Fallback gate-evidence reuse (Issue 08 artifacts are optional): a completed
460
- * Task-final Round whose evidence commit exactly matches the candidate's
461
- * primary head may donate its checks by digest. A changed head returns null,
462
- * so an old GREEN can never be reused against a different tree.
463
- */
464
- export function reusableTaskReviewEvidence(store, taskId, candidate) {
465
- const rounds = store.listReviewRounds(taskId)
466
- .filter((round) => (round.scope ?? "work-item") === "task"
467
- && isSemanticReviewRound(round, store)
468
- && round.evidenceCommit !== undefined
469
- && isSameTaskReviewCandidate(round.taskCandidate, candidate))
470
- .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
471
- const latest = rounds.at(-1);
472
- if (latest === undefined || latest.evidenceCommit === undefined)
473
- return null;
474
- const checks = latest.checks ?? [];
475
- if (checks.length === 0 || checks.some((check) => check.outcome !== "passed"))
476
- return null;
477
- const digest = createHash("sha256")
478
- .update(JSON.stringify(checks))
479
- .digest("hex");
480
- return {
481
- reviewRoundId: latest.id,
482
- evidenceCommit: latest.evidenceCommit,
483
- digest,
484
- checks
485
- };
486
- }
487
- /**
488
- * Builds the Issue 06 incremental Review context. The Reviewer receives the
489
- * cross-Round ledger, exact old/new frozen heads, repair evidence, and any
490
- * reusable exact-head checks. A changed head or failed/absent checks returns
491
- * no reusable evidence, so an old GREEN can never be borrowed for a new tree.
492
- */
493
- export function buildTaskFinalReviewFindingContext(store, taskId, candidate) {
494
- const previousSemanticRound = store.listReviewRounds(taskId)
495
- .filter((round) => (round.scope ?? "work-item") === "task"
496
- && isSemanticReviewRound(round, store))
497
- .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }))
498
- .at(-1) ?? null;
499
- const reusableEvidence = reusableTaskReviewEvidence(store, taskId, candidate);
500
- // dbonly: the finding ledger is SQLite-native. A file-only Home (no yui.db)
501
- // cannot serve findings; the Review dispatch must still proceed, so the
502
- // ledger context degrades to an explicit "unavailable" note.
503
- let ledgerContext;
504
- try {
505
- ledgerContext = renderFindingLedgerContext(summarizeFindingLedger(store, taskId));
506
- }
507
- catch {
508
- ledgerContext = "Finding ledger: unavailable (a current SQLite Home is required).";
509
- }
510
- const lines = [
511
- "Review convergence context:",
512
- ledgerContext,
513
- ...(previousSemanticRound === null
514
- ? ["Previous semantic Task-final ReviewRound: none."]
515
- : [
516
- `Previous semantic Task-final ReviewRound: ${previousSemanticRound.id}`,
517
- ...candidate.projects.map((project) => {
518
- const previous = previousSemanticRound.taskCandidate?.projects
519
- .find((entry) => entry.projectId === project.projectId)?.commit;
520
- return previous === undefined
521
- ? `Exact review boundary for ${project.projectId}: new head ${project.commit}`
522
- : previous === project.commit
523
- ? `Exact review boundary for ${project.projectId}: unchanged at ${project.commit}`
524
- : `Exact diff for ${project.projectId}: ${previous}..${project.commit}`;
525
- })
526
- ]),
527
- ...(reusableEvidence === null
528
- ? [
529
- "Reusable gate evidence: none (head changed, checks are absent, or a check did not pass); rerun the checks needed for this exact tree."
530
- ]
531
- : [
532
- `Reusable gate evidence: ${reusableEvidence.reviewRoundId}@${reusableEvidence.evidenceCommit}`,
533
- `Reusable check digest: ${reusableEvidence.digest}`,
534
- `Reusable checks: ${reusableEvidence.checks.map(({ name, outcome }) => `${name}=${outcome}`).join(", ")}`
535
- ]),
536
- "Reuse each listed ledger finding id when it is still valid; explain why a new finding is not already covered by the ledger.",
537
- "Your final report must clearly list verified-fixed findings, new findings, accepted risks, and residual verification gaps."
538
- ];
539
- return {
540
- context: lines.join("\n"),
541
- previousSemanticRound,
542
- reusableEvidence
543
- };
544
- }
545
- function isSameTaskReviewCandidate(left, right) {
546
- return left !== undefined
547
- && left.projects.length === right.projects.length
548
- && left.projects.every((project, index) => (project.projectId === right.projects[index]?.projectId
549
- && project.commit === right.projects[index]?.commit));
550
- }