@straiffi/archon 1.7.6 → 1.7.8

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 (158) hide show
  1. package/dist/client/assets/ActivityView-DwfDSF4q.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-3dMCYCSZ.js +1 -0
  3. package/dist/client/assets/VerificationDialog-CoTfXMyb.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-D0PXGvSL.js → architectureDiagram-3BPJPVTR-Bdnc97XK.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-BCLAxnp5.js → blockDiagram-GPEHLZMM-boxM3She.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-DVZ5yZWV.js → c4Diagram-AAUBKEIU-CJDdF7xd.js} +1 -1
  7. package/dist/client/assets/channel-BGqdAcIT.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-BGJYj1Lu.js → chunk-2J33WTMH-ClpcO-Om.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-DI5-yK3m.js → chunk-3OPIFGDE-CcuL_65J.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-Dnr5DDsb.js → chunk-5ZQYHXKU-CUGYZXkH.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-8HdFwn-P.js → chunk-727SXJPM-CApeLDyK.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-C7x9t2uJ.js → chunk-AQP2D5EJ-8FdSvF2D.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-CrhwRPh5.js → chunk-CSCIHK7Q-CZoIOF3a.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-CdD62r5U.js → chunk-KSCS5N6A-BigFQI5y.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-CqQVvwc_.js → chunk-L5ZTLDWV-DIi3SHaT.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-DLHrqoJ4.js → chunk-LZXEDZCA-B-Z0BcQy.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-B0yOdiPF.js → chunk-ND2GUHAM-EBeba0mg.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-boM-MocY.js → chunk-NZK2D7GU-DJBI2y53.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-B0jgJn0t.js → chunk-O5CBEL6O-BqxdXhMd.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-C_S_v-m2.js → chunk-WU5MYG2G-Coe1_Cbb.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-D5VuKRcj.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B9GNqpRr.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-DKl6Y8ya.js → dagre-BM42HDAG-tuTmeA0Y.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-6Lyn9DBp.js → diagram-2AECGRRQ-ILGCpG2F.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-wdS1vEkN.js → diagram-5GNKFQAL-C8qBZsgK.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-CPih29jF.js → diagram-KO2AKTUF-BXlcm_Zo.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-Cr5Px74d.js → diagram-LMA3HP47-B8V5EVbD.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-i0PjSScd.js → diagram-OG6HWLK6-CWsdOqcP.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-Dt4yOC9J.js → erDiagram-TEJ5UH35-DW8pgly2.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-Bf6kvajC.js → flowDiagram-I6XJVG4X-Djn7fOVR.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DgxePMk_.js → ganttDiagram-6RSMTGT7-DZ421MEy.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BZq2cmFS.js → gitGraphDiagram-PVQCEYII-CCNzLPuP.js} +1 -1
  33. package/dist/client/assets/index-BRc02lbW.js +273 -0
  34. package/dist/client/assets/index-CZO9HL1Z.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-Chij69KN.js → infoDiagram-5YYISTIA-Bd1gYgMp.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-3qHCLFgD.js → ishikawaDiagram-YF4QCWOH-txyMoatJ.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-DvRW8tbA.js → journeyDiagram-JHISSGLW-DhT3bmX6.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-DjR2T5I1.js → kanban-definition-UN3LZRKU-BBHt2hfC.js} +1 -1
  39. package/dist/client/assets/{line-B9OzrQ1Y.js → line-KGRcT7Qz.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-DYcDor4d.js → mermaid-parser.core-CDzt4_zR.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-DG8QYo2u.js → mermaid.core-DD_-k1aB.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-f4QPF6b3.js → mindmap-definition-RKZ34NQL-DoctVTDl.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-CMkl2jBu.js → pieDiagram-4H26LBE5-DFlLEi70.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-BXaUHzJi.js → quadrantDiagram-W4KKPZXB-3YHOQgl7.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-B-Inuj_6.js → requirementDiagram-4Y6WPE33-X4fZVL9M.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BjsmZFYI.js → sankeyDiagram-5OEKKPKP-BUPnMXeG.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-B1PAm6sX.js → sequenceDiagram-3UESZ5HK-D6QU3M64.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-pLONNEN8.js → stateDiagram-AJRCARHV-c35bu4qZ.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ymy1_EHr.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-BLMkSXo_.js → timeline-definition-PNZ67QCA-CX2y6e_b.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-CEwGRegG.js → vennDiagram-CIIHVFJN-BDaWltjF.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-_md2IBXC.js → wardleyDiagram-YWT4CUSO-BpBRL2sb.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DVzwfXHc.js → xychartDiagram-2RQKCTM6-Bqw7X0UT.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +460 -2
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +389 -47
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/activeAutomationChild.js +31 -0
  60. package/dist/server/lib/activeAutomationChild.js.map +1 -0
  61. package/dist/server/lib/automationCheckGate.js +266 -0
  62. package/dist/server/lib/automationCheckGate.js.map +1 -0
  63. package/dist/server/lib/automationProgression.js +20 -21
  64. package/dist/server/lib/automationProgression.js.map +1 -1
  65. package/dist/server/lib/automationReconciliation.js +16 -2
  66. package/dist/server/lib/automationReconciliation.js.map +1 -1
  67. package/dist/server/lib/automationResumeDecision.js +4 -1
  68. package/dist/server/lib/automationResumeDecision.js.map +1 -1
  69. package/dist/server/lib/automationStageDetails.js +173 -41
  70. package/dist/server/lib/automationStageDetails.js.map +1 -1
  71. package/dist/server/lib/chatMessages.js +37 -8
  72. package/dist/server/lib/chatMessages.js.map +1 -1
  73. package/dist/server/lib/chatOperations.js +119 -68
  74. package/dist/server/lib/chatOperations.js.map +1 -1
  75. package/dist/server/lib/checkDefinitions.js +278 -0
  76. package/dist/server/lib/checkDefinitions.js.map +1 -0
  77. package/dist/server/lib/checkGateEvaluations.js +294 -0
  78. package/dist/server/lib/checkGateEvaluations.js.map +1 -0
  79. package/dist/server/lib/checkRunner.js +674 -0
  80. package/dist/server/lib/checkRunner.js.map +1 -0
  81. package/dist/server/lib/checkRuns.js +736 -0
  82. package/dist/server/lib/checkRuns.js.map +1 -0
  83. package/dist/server/lib/checkWorkspaceIdentity.js +207 -0
  84. package/dist/server/lib/checkWorkspaceIdentity.js.map +1 -0
  85. package/dist/server/lib/executionReconciliation.js +79 -2
  86. package/dist/server/lib/executionReconciliation.js.map +1 -1
  87. package/dist/server/lib/executionRetry.js +14 -6
  88. package/dist/server/lib/executionRetry.js.map +1 -1
  89. package/dist/server/lib/executionRetryContext.js +3 -3
  90. package/dist/server/lib/executionRetryContext.js.map +1 -1
  91. package/dist/server/lib/executionRunRetention.js +41 -3
  92. package/dist/server/lib/executionRunRetention.js.map +1 -1
  93. package/dist/server/lib/executionRuns.js +135 -2
  94. package/dist/server/lib/executionRuns.js.map +1 -1
  95. package/dist/server/lib/executionTimeline.js +52 -1
  96. package/dist/server/lib/executionTimeline.js.map +1 -1
  97. package/dist/server/lib/mobileAccess.js +44 -6
  98. package/dist/server/lib/mobileAccess.js.map +1 -1
  99. package/dist/server/lib/planModeQuestions.js +12 -7
  100. package/dist/server/lib/planModeQuestions.js.map +1 -1
  101. package/dist/server/lib/planQuestionOperations.js +246 -0
  102. package/dist/server/lib/planQuestionOperations.js.map +1 -0
  103. package/dist/server/lib/planQuestions.js +139 -0
  104. package/dist/server/lib/planQuestions.js.map +1 -0
  105. package/dist/server/lib/projectAutoConfig.js +47 -65
  106. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  107. package/dist/server/lib/projects.js +87 -7
  108. package/dist/server/lib/projects.js.map +1 -1
  109. package/dist/server/lib/realtime.js +24 -1
  110. package/dist/server/lib/realtime.js.map +1 -1
  111. package/dist/server/lib/requiredCheckPolicy.js +68 -0
  112. package/dist/server/lib/requiredCheckPolicy.js.map +1 -0
  113. package/dist/server/lib/shutdown.js +4 -4
  114. package/dist/server/lib/shutdown.js.map +1 -1
  115. package/dist/server/lib/specArtifact.js +36 -38
  116. package/dist/server/lib/specArtifact.js.map +1 -1
  117. package/dist/server/lib/specMessages.js +31 -18
  118. package/dist/server/lib/specMessages.js.map +1 -1
  119. package/dist/server/lib/specOperations.js +129 -78
  120. package/dist/server/lib/specOperations.js.map +1 -1
  121. package/dist/server/lib/testCommandRunner.js +3 -2
  122. package/dist/server/lib/testCommandRunner.js.map +1 -1
  123. package/dist/server/lib/testSessions.js +2 -6
  124. package/dist/server/lib/testSessions.js.map +1 -1
  125. package/dist/server/lib/ticketAutomationCommit.js +185 -22
  126. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  127. package/dist/server/lib/ticketFollowUpOperations.js +19 -6
  128. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  129. package/dist/server/lib/ticketMessages.js +38 -9
  130. package/dist/server/lib/ticketMessages.js.map +1 -1
  131. package/dist/server/lib/ticketWorkflowOperations.js +10 -1
  132. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  133. package/dist/server/lib/tickets.js +21 -1
  134. package/dist/server/lib/tickets.js.map +1 -1
  135. package/dist/server/lib/types.js +2 -0
  136. package/dist/server/lib/types.js.map +1 -1
  137. package/dist/server/workers/chat.js +48 -2
  138. package/dist/server/workers/chat.js.map +1 -1
  139. package/dist/server/workers/followUp.js +50 -2
  140. package/dist/server/workers/followUp.js.map +1 -1
  141. package/dist/server/workers/plan.js +57 -4
  142. package/dist/server/workers/plan.js.map +1 -1
  143. package/dist/server/workers/planFollowUp.js +51 -2
  144. package/dist/server/workers/planFollowUp.js.map +1 -1
  145. package/dist/server/workers/review.js +2 -10
  146. package/dist/server/workers/review.js.map +1 -1
  147. package/dist/server/workers/specSession.js +55 -5
  148. package/dist/server/workers/specSession.js.map +1 -1
  149. package/package.json +1 -1
  150. package/dist/client/assets/ActivityView-BrZY8vYf.js +0 -1
  151. package/dist/client/assets/GitHubActionsDialog-CUfdVw97.js +0 -1
  152. package/dist/client/assets/TestsDialog-FXcnLOyf.js +0 -4
  153. package/dist/client/assets/channel-BCPCrThZ.js +0 -1
  154. package/dist/client/assets/classDiagram-4FO5ZUOK-BdJlc_1i.js +0 -1
  155. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Bidw5Zc1.js +0 -1
  156. package/dist/client/assets/index-BJGHMaMz.css +0 -2
  157. package/dist/client/assets/index-CjRMYO1U.js +0 -273
  158. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-kWPDF3wr.js +0 -1
@@ -0,0 +1,736 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import db from '../db.js';
3
+ import { getCheckDefinitionFingerprint, normalizeCheckDefinitions } from './checkDefinitions.js';
4
+ export const CHECK_ATTEMPT_OUTCOMES = [
5
+ 'passed',
6
+ 'command_failure',
7
+ 'spawn_failure',
8
+ 'timeout',
9
+ 'workspace_changed_before_start',
10
+ 'user_stop',
11
+ 'restart_loss',
12
+ 'shutdown_loss',
13
+ ];
14
+ export const toCheckAttemptSummary = ({ retained_output: _retainedOutput, ...summary }) => summary;
15
+ const canonicalDefinition = (definition) => ({
16
+ id: definition.id,
17
+ name: definition.name,
18
+ category: definition.category,
19
+ command: definition.command,
20
+ ...(definition.cwd ? { cwd: definition.cwd } : {}),
21
+ timeout_ms: definition.timeout_ms,
22
+ ...(definition.category === 'test' ? { runner: definition.runner } : {}),
23
+ });
24
+ const canonicalWorkspaceIdentity = (identity) => identity.state === 'available'
25
+ ? { state: 'available', algorithm: 'sha256', fingerprint: identity.fingerprint, head: identity.head }
26
+ : { state: 'unavailable', reason: identity.reason, message: identity.message };
27
+ const assertSha256 = (value, name) => {
28
+ if (!/^[a-f0-9]{64}$/.test(value)) {
29
+ throw new Error(`${name} must be a lowercase SHA-256 fingerprint`);
30
+ }
31
+ };
32
+ const assertWorkspaceIdentity = (identity, name) => {
33
+ if (identity.state === 'available') {
34
+ assertSha256(identity.fingerprint, `${name} fingerprint`);
35
+ if (!/^[a-f0-9]{40,64}$/.test(identity.head)) {
36
+ throw new Error(`${name} HEAD must be a Git object ID`);
37
+ }
38
+ return;
39
+ }
40
+ if (!identity.reason || !identity.message) {
41
+ throw new Error(`${name} unavailable identity requires a reason and message`);
42
+ }
43
+ };
44
+ const updateLengthDelimited = (hash, value) => {
45
+ const buffer = Buffer.from(value, 'utf8');
46
+ const length = Buffer.alloc(8);
47
+ length.writeBigUInt64BE(BigInt(buffer.length));
48
+ hash.update(length).update(buffer);
49
+ };
50
+ const compareDefinitionIds = (left, right) => (Buffer.compare(Buffer.from(left.id, 'utf8'), Buffer.from(right.id, 'utf8')));
51
+ export const getCheckConfigurationFingerprint = (target, definitions) => {
52
+ const hash = createHash('sha256');
53
+ updateLengthDelimited(hash, 'archon-check-configuration-v1');
54
+ updateLengthDelimited(hash, target.targetKind);
55
+ updateLengthDelimited(hash, target.targetBundleId ?? '');
56
+ for (const definition of [...definitions].sort(compareDefinitionIds)) {
57
+ updateLengthDelimited(hash, definition.id);
58
+ updateLengthDelimited(hash, getCheckDefinitionFingerprint(definition));
59
+ }
60
+ return hash.digest('hex');
61
+ };
62
+ export const mapCheckAttemptOutcomeToExecutionStatus = (outcome) => {
63
+ if (outcome === 'passed') {
64
+ return 'completed';
65
+ }
66
+ if (outcome === 'user_stop') {
67
+ return 'cancelled';
68
+ }
69
+ if (outcome === 'restart_loss' || outcome === 'shutdown_loss') {
70
+ return 'interrupted';
71
+ }
72
+ return 'failed';
73
+ };
74
+ const parseJson = (value) => JSON.parse(value);
75
+ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled', 'interrupted']);
76
+ export const CHECK_ATTEMPT_OUTPUT_LIMIT_BYTES = 256 * 1024;
77
+ const retainUtf8Suffix = (value, byteLimit) => {
78
+ if (Buffer.byteLength(value, 'utf8') <= byteLimit) {
79
+ return value;
80
+ }
81
+ let low = 0;
82
+ let high = value.length;
83
+ while (low < high) {
84
+ const midpoint = Math.floor((low + high) / 2);
85
+ if (Buffer.byteLength(value.slice(midpoint), 'utf8') <= byteLimit) {
86
+ high = midpoint;
87
+ }
88
+ else {
89
+ low = midpoint + 1;
90
+ }
91
+ }
92
+ let start = low;
93
+ if (start > 0 && start < value.length) {
94
+ const previous = value.charCodeAt(start - 1);
95
+ const next = value.charCodeAt(start);
96
+ if (previous >= 0xd800 && previous <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
97
+ start += 1;
98
+ }
99
+ }
100
+ return value.slice(start);
101
+ };
102
+ const assertBatchTarget = (projectId, details) => {
103
+ if (details.targetKind === 'repo_root') {
104
+ if (details.targetBundleId != null) {
105
+ throw new Error('Repository-root check batches cannot have a target bundle');
106
+ }
107
+ return;
108
+ }
109
+ const bundle = db.prepare(`
110
+ SELECT 1 FROM worktree_bundles
111
+ WHERE id = ? AND project_id = ? AND kind <> 'project_root'
112
+ `).get(details.targetBundleId, projectId);
113
+ if (!bundle) {
114
+ throw new Error(`Check target bundle ${details.targetBundleId} does not belong to project ${projectId}`);
115
+ }
116
+ };
117
+ const getRun = (projectId, runId) => db.prepare(`
118
+ SELECT id, project_id, kind, status, parent_run_id, correlation_id
119
+ FROM execution_runs WHERE id = ? AND project_id = ?
120
+ `).get(runId, projectId);
121
+ export const validateCheckBatchDetails = (projectId, details) => {
122
+ assertBatchTarget(projectId, details);
123
+ assertWorkspaceIdentity(details.initialWorkspaceIdentity, 'Initial workspace identity');
124
+ if (details.selectedDefinitions.length === 0) {
125
+ throw new Error('Check batches require at least one selected definition');
126
+ }
127
+ const normalized = normalizeCheckDefinitions(details.selectedDefinitions);
128
+ if ('error' in normalized) {
129
+ throw new Error(`Invalid selected check definitions: ${normalized.error}`);
130
+ }
131
+ const definitionIds = new Set();
132
+ for (const definition of details.selectedDefinitions) {
133
+ if (!definition.id || definitionIds.has(definition.id)) {
134
+ throw new Error('Selected check definition IDs must be non-empty and unique');
135
+ }
136
+ definitionIds.add(definition.id);
137
+ assertSha256(getCheckDefinitionFingerprint(definition), 'Definition fingerprint');
138
+ }
139
+ const expectedFingerprint = getCheckConfigurationFingerprint(details, details.selectedDefinitions);
140
+ const fingerprint = details.configurationFingerprint ?? expectedFingerprint;
141
+ assertSha256(fingerprint, 'Check configuration fingerprint');
142
+ if (fingerprint !== expectedFingerprint) {
143
+ throw new Error('Check configuration fingerprint does not match the selected definitions and target');
144
+ }
145
+ if (details.rerunOfBatchRunId) {
146
+ const source = getRun(projectId, details.rerunOfBatchRunId);
147
+ if (!source || source.kind !== 'check_batch' || !['completed', 'failed', 'cancelled', 'interrupted'].includes(source.status)) {
148
+ throw new Error('Check rerun source must be a check batch in the same project');
149
+ }
150
+ }
151
+ return fingerprint;
152
+ };
153
+ export const insertCheckBatchDetails = (projectId, runId, details) => {
154
+ const fingerprint = validateCheckBatchDetails(projectId, details);
155
+ db.prepare(`
156
+ INSERT INTO check_batch_details (
157
+ execution_run_id, target_kind, target_bundle_id, target_label,
158
+ selected_definition_snapshots_json, initial_workspace_identity_json,
159
+ configuration_fingerprint, stop_requested, rerun_of_batch_run_id
160
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
161
+ `).run(runId, details.targetKind, details.targetBundleId ?? null, details.targetLabel ?? null, JSON.stringify(details.selectedDefinitions.map(canonicalDefinition)), JSON.stringify(canonicalWorkspaceIdentity(details.initialWorkspaceIdentity)), fingerprint, details.stopRequested ? 1 : 0, details.rerunOfBatchRunId ?? null);
162
+ };
163
+ export const validateCheckAttemptDetails = (projectId, runId, details) => {
164
+ assertWorkspaceIdentity(details.initialWorkspaceIdentity, 'Initial workspace identity');
165
+ const normalized = normalizeCheckDefinitions([details.definition]);
166
+ if ('error' in normalized) {
167
+ throw new Error(`Invalid check attempt definition: ${normalized.error}`);
168
+ }
169
+ const batch = getRun(projectId, details.batchRunId);
170
+ if (!batch || batch.kind !== 'check_batch') {
171
+ throw new Error('Check attempts require a check batch parent in the same project');
172
+ }
173
+ const run = getRun(projectId, runId);
174
+ if (!run || run.kind !== 'check_attempt' || run.parent_run_id !== batch.id || run.correlation_id !== batch.correlation_id) {
175
+ throw new Error('Check attempt execution lineage must match its batch details');
176
+ }
177
+ const selected = db.prepare('SELECT selected_definition_snapshots_json FROM check_batch_details WHERE execution_run_id = ?')
178
+ .get(batch.id);
179
+ const selectedDefinitions = selected ? parseJson(selected.selected_definition_snapshots_json) : [];
180
+ const selectedDefinition = selectedDefinitions.find(definition => definition.id === details.definition.id);
181
+ if (!selectedDefinition || getCheckDefinitionFingerprint(selectedDefinition) !== getCheckDefinitionFingerprint(details.definition)) {
182
+ throw new Error('Check attempt definition must match an immutable batch definition snapshot');
183
+ }
184
+ if (details.rerunOfAttemptRunId) {
185
+ const source = db.prepare(`
186
+ SELECT source_run.project_id, source_run.kind, source_run.status, source.definition_id, source.batch_run_id,
187
+ source.outcome,
188
+ source_batch.rerun_of_batch_run_id
189
+ FROM execution_runs source_run
190
+ JOIN check_attempt_details source ON source.execution_run_id = source_run.id
191
+ JOIN check_batch_details source_batch ON source_batch.execution_run_id = ?
192
+ WHERE source_run.id = ?
193
+ `).get(batch.id, details.rerunOfAttemptRunId);
194
+ if (!source
195
+ || source.project_id !== projectId
196
+ || source.kind !== 'check_attempt'
197
+ || !['completed', 'failed', 'cancelled', 'interrupted'].includes(source.status)
198
+ || source.outcome === null
199
+ || source.definition_id !== details.definition.id
200
+ || source.batch_run_id !== source.rerun_of_batch_run_id) {
201
+ throw new Error('Check rerun attempt must match the stable definition ID in the source batch');
202
+ }
203
+ }
204
+ };
205
+ export const insertCheckAttemptDetails = (projectId, runId, details) => {
206
+ validateCheckAttemptDetails(projectId, runId, details);
207
+ const definition = canonicalDefinition(details.definition);
208
+ db.prepare(`
209
+ INSERT INTO check_attempt_details (
210
+ execution_run_id, batch_run_id, definition_id, command_snapshot_json,
211
+ definition_fingerprint, initial_workspace_identity_json, rerun_of_attempt_run_id
212
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
213
+ `).run(runId, details.batchRunId, definition.id, JSON.stringify(definition), getCheckDefinitionFingerprint(definition), JSON.stringify(canonicalWorkspaceIdentity(details.initialWorkspaceIdentity)), details.rerunOfAttemptRunId ?? null);
214
+ };
215
+ export const getCheckBatchDetails = (projectId, runId) => {
216
+ const row = db.prepare(`
217
+ SELECT details.*, run.project_id, run.status AS execution_status
218
+ FROM check_batch_details details
219
+ JOIN execution_runs run ON run.id = details.execution_run_id
220
+ WHERE run.project_id = ? AND run.id = ?
221
+ `).get(projectId, runId);
222
+ if (!row) {
223
+ return null;
224
+ }
225
+ const { selected_definition_snapshots_json, initial_workspace_identity_json, ...details } = row;
226
+ return {
227
+ ...details,
228
+ stop_requested: row.stop_requested === 1,
229
+ selected_definition_snapshots: parseJson(selected_definition_snapshots_json),
230
+ initial_workspace_identity: parseJson(initial_workspace_identity_json),
231
+ };
232
+ };
233
+ export const listCheckBatchDetailsForRuns = (projectId, runIds) => {
234
+ const ids = [...new Set(runIds)];
235
+ if (ids.length === 0) {
236
+ return new Map();
237
+ }
238
+ const rows = db.prepare(`
239
+ SELECT details.*, run.project_id, run.status AS execution_status
240
+ FROM check_batch_details details
241
+ JOIN execution_runs run ON run.id = details.execution_run_id
242
+ WHERE run.project_id = ? AND run.id IN (${ids.map(() => '?').join(', ')})
243
+ `).all(projectId, ...ids);
244
+ return new Map(rows.map(row => {
245
+ const { selected_definition_snapshots_json, initial_workspace_identity_json, ...details } = row;
246
+ return [row.execution_run_id, {
247
+ ...details,
248
+ stop_requested: row.stop_requested === 1,
249
+ selected_definition_snapshots: parseJson(selected_definition_snapshots_json),
250
+ initial_workspace_identity: parseJson(initial_workspace_identity_json),
251
+ }];
252
+ }));
253
+ };
254
+ export const getCheckAttemptDetails = (projectId, runId) => {
255
+ const row = db.prepare(`
256
+ SELECT details.*, run.project_id, run.status AS execution_status
257
+ FROM check_attempt_details details
258
+ JOIN execution_runs run ON run.id = details.execution_run_id
259
+ WHERE run.project_id = ? AND run.id = ?
260
+ `).get(projectId, runId);
261
+ if (!row) {
262
+ return null;
263
+ }
264
+ const { command_snapshot_json, initial_workspace_identity_json, final_workspace_identity_json, ...details } = row;
265
+ return {
266
+ ...details,
267
+ command_snapshot: parseJson(command_snapshot_json),
268
+ initial_workspace_identity: parseJson(initial_workspace_identity_json),
269
+ final_workspace_identity: final_workspace_identity_json ? parseJson(final_workspace_identity_json) : null,
270
+ };
271
+ };
272
+ export const listCheckAttemptDetailsForRuns = (projectId, runIds) => {
273
+ const ids = [...new Set(runIds)];
274
+ if (ids.length === 0) {
275
+ return new Map();
276
+ }
277
+ const rows = db.prepare(`
278
+ SELECT details.*, run.project_id, run.status AS execution_status
279
+ FROM check_attempt_details details
280
+ JOIN execution_runs run ON run.id = details.execution_run_id
281
+ WHERE run.project_id = ? AND run.id IN (${ids.map(() => '?').join(', ')})
282
+ `).all(projectId, ...ids);
283
+ return new Map(rows.map(row => {
284
+ const { command_snapshot_json, initial_workspace_identity_json, final_workspace_identity_json, ...details } = row;
285
+ return [row.execution_run_id, {
286
+ ...details,
287
+ command_snapshot: parseJson(command_snapshot_json),
288
+ initial_workspace_identity: parseJson(initial_workspace_identity_json),
289
+ final_workspace_identity: final_workspace_identity_json ? parseJson(final_workspace_identity_json) : null,
290
+ }];
291
+ }));
292
+ };
293
+ export const listCheckAttemptDetails = (projectId, batchRunId) => {
294
+ const rows = db.prepare(`
295
+ SELECT details.*, run.project_id, run.status AS execution_status
296
+ FROM check_attempt_details details
297
+ JOIN execution_runs run ON run.id = details.execution_run_id
298
+ WHERE run.project_id = ? AND details.batch_run_id = ?
299
+ ORDER BY run.rowid ASC
300
+ `).all(projectId, batchRunId);
301
+ return rows.map(row => {
302
+ const { command_snapshot_json, initial_workspace_identity_json, final_workspace_identity_json, ...details } = row;
303
+ return {
304
+ ...details,
305
+ command_snapshot: parseJson(command_snapshot_json),
306
+ initial_workspace_identity: parseJson(initial_workspace_identity_json),
307
+ final_workspace_identity: final_workspace_identity_json ? parseJson(final_workspace_identity_json) : null,
308
+ };
309
+ });
310
+ };
311
+ export const listCheckAttemptDetailsForBatches = (projectId, batchRunIds) => {
312
+ const ids = [...new Set(batchRunIds)];
313
+ const attemptsByBatchId = new Map(ids.map(id => [id, []]));
314
+ if (ids.length === 0) {
315
+ return attemptsByBatchId;
316
+ }
317
+ const rows = db.prepare(`
318
+ SELECT details.*, run.project_id, run.status AS execution_status
319
+ FROM check_attempt_details details
320
+ JOIN execution_runs run ON run.id = details.execution_run_id
321
+ WHERE run.project_id = ? AND details.batch_run_id IN (${ids.map(() => '?').join(', ')})
322
+ ORDER BY run.created_at ASC, run.id ASC
323
+ `).all(projectId, ...ids);
324
+ for (const row of rows) {
325
+ const { command_snapshot_json, initial_workspace_identity_json, final_workspace_identity_json, ...details } = row;
326
+ attemptsByBatchId.get(row.batch_run_id)?.push({
327
+ ...details,
328
+ command_snapshot: parseJson(command_snapshot_json),
329
+ initial_workspace_identity: parseJson(initial_workspace_identity_json),
330
+ final_workspace_identity: final_workspace_identity_json ? parseJson(final_workspace_identity_json) : null,
331
+ });
332
+ }
333
+ return attemptsByBatchId;
334
+ };
335
+ const resolveRerunDefinitionIds = (selection, sourceAttempts) => {
336
+ if (selection.mode === 'all') {
337
+ return sourceAttempts.map(attempt => attempt.definition_id);
338
+ }
339
+ if (selection.mode === 'failed') {
340
+ return sourceAttempts
341
+ .filter(attempt => attempt.outcome !== 'passed')
342
+ .map(attempt => attempt.definition_id);
343
+ }
344
+ if (selection.definitionIds.length === 0 || selection.definitionIds.length !== new Set(selection.definitionIds).size) {
345
+ throw new Error('Selected check reruns require at least one unique source definition ID');
346
+ }
347
+ return selection.definitionIds;
348
+ };
349
+ export const resolveCheckBatchRerunEligibility = (input) => {
350
+ const sourceBatch = getCheckBatchDetails(input.projectId, input.batchRunId);
351
+ const attempts = sourceBatch ? listCheckAttemptDetails(input.projectId, sourceBatch.execution_run_id) : [];
352
+ return resolveCheckBatchRerunEligibilityFromDetails({
353
+ batch: sourceBatch,
354
+ attempts,
355
+ availableDefinitions: input.availableDefinitions,
356
+ selection: input.selection,
357
+ targetAvailable: input.targetAvailable,
358
+ workspaceAvailable: input.workspaceAvailable,
359
+ targetBusy: Boolean(sourceBatch && findActiveCheckBatchForTarget(input.projectId, sourceBatch.target_kind, sourceBatch.target_bundle_id)),
360
+ });
361
+ };
362
+ export const resolveCheckBatchRerunEligibilityFromDetails = (input) => {
363
+ const sourceBatch = input.batch;
364
+ if (!sourceBatch || !TERMINAL_STATUSES.has(sourceBatch.execution_status)) {
365
+ return { eligible: false, reason: 'status_not_retryable' };
366
+ }
367
+ if (!input.targetAvailable) {
368
+ return { eligible: false, reason: 'retry_target_missing' };
369
+ }
370
+ if (!input.workspaceAvailable) {
371
+ return { eligible: false, reason: 'retry_workspace_unavailable' };
372
+ }
373
+ if (input.targetBusy) {
374
+ return { eligible: false, reason: 'retry_target_busy' };
375
+ }
376
+ const attempts = input.attempts;
377
+ if (attempts.length !== sourceBatch.selected_definition_snapshots.length || attempts.length === 0 || attempts.some(attempt => attempt.outcome === null)) {
378
+ return { eligible: false, reason: 'retry_input_unavailable' };
379
+ }
380
+ const availableIds = new Set(input.availableDefinitions.map(definition => definition.id));
381
+ let definitionIds;
382
+ try {
383
+ definitionIds = resolveRerunDefinitionIds(input.selection ?? { mode: 'all' }, attempts);
384
+ }
385
+ catch {
386
+ return { eligible: false, reason: 'retry_input_unavailable' };
387
+ }
388
+ const sourceIds = new Set(attempts.map(attempt => attempt.definition_id));
389
+ if (definitionIds.length === 0 || definitionIds.some(id => !sourceIds.has(id) || !availableIds.has(id))) {
390
+ return { eligible: false, reason: 'retry_input_unavailable' };
391
+ }
392
+ return { eligible: true, reason: null };
393
+ };
394
+ const getCheckTargetKey = (targetKind, targetBundleId) => (`${targetKind}:${targetBundleId ?? ''}`);
395
+ export const listActiveCheckBatchTargetKeys = (projectId) => {
396
+ const rows = db.prepare(`
397
+ SELECT details.target_kind, details.target_bundle_id
398
+ FROM execution_runs run
399
+ JOIN check_batch_details details ON details.execution_run_id = run.id
400
+ WHERE run.project_id = ? AND run.status IN ('queued', 'waiting', 'running')
401
+ `).all(projectId);
402
+ return new Set(rows.map(row => getCheckTargetKey(row.target_kind, row.target_bundle_id)));
403
+ };
404
+ export const isCheckBatchTargetActive = (activeTargetKeys, batch) => (activeTargetKeys.has(getCheckTargetKey(batch.target_kind, batch.target_bundle_id)));
405
+ export const listCheckBatchTargetStates = (projectId, batches) => {
406
+ const bundleIds = [...new Set(batches.flatMap(batch => batch.target_kind === 'bundle' && batch.target_bundle_id
407
+ ? [batch.target_bundle_id]
408
+ : []))];
409
+ const bundles = bundleIds.length === 0
410
+ ? []
411
+ : db.prepare(`
412
+ SELECT id, branch, kind FROM worktree_bundles
413
+ WHERE project_id = ? AND id IN (${bundleIds.map(() => '?').join(', ')})
414
+ `).all(projectId, ...bundleIds);
415
+ const bundlesById = new Map(bundles.map(bundle => [bundle.id, bundle]));
416
+ return new Map(batches.map(batch => {
417
+ if (batch.target_kind === 'repo_root') {
418
+ return [batch.execution_run_id, { targetAvailable: true, bundleBranch: null }];
419
+ }
420
+ const bundle = batch.target_bundle_id ? bundlesById.get(batch.target_bundle_id) : undefined;
421
+ return [batch.execution_run_id, {
422
+ targetAvailable: Boolean(bundle && bundle.kind !== 'project_root'),
423
+ bundleBranch: bundle?.kind === 'project_root' ? null : bundle?.branch ?? null,
424
+ }];
425
+ }));
426
+ };
427
+ export const getCheckBatchTargetState = (projectId, batch) => {
428
+ if (batch.target_kind === 'repo_root') {
429
+ return { targetAvailable: true, bundleBranch: null };
430
+ }
431
+ const bundle = db.prepare(`
432
+ SELECT branch, kind FROM worktree_bundles WHERE id = ? AND project_id = ?
433
+ `).get(batch.target_bundle_id, projectId);
434
+ return {
435
+ targetAvailable: Boolean(bundle && bundle.kind !== 'project_root'),
436
+ bundleBranch: bundle?.kind === 'project_root' ? null : bundle?.branch ?? null,
437
+ };
438
+ };
439
+ export const rerunCheckBatch = (input) => {
440
+ const sourceBatch = getCheckBatchDetails(input.projectId, input.sourceBatchRunId);
441
+ if (!sourceBatch || !TERMINAL_STATUSES.has(sourceBatch.execution_status)) {
442
+ throw new Error('Check rerun source must be a terminal batch in the same project');
443
+ }
444
+ assertWorkspaceIdentity(input.initialWorkspaceIdentity, 'Initial workspace identity');
445
+ if (input.initialWorkspaceIdentity.state !== 'available') {
446
+ throw new Error(`Check workspace identity is unavailable: ${input.initialWorkspaceIdentity.message}`);
447
+ }
448
+ const conflict = findActiveCheckBatchForTarget(input.projectId, sourceBatch.target_kind, sourceBatch.target_bundle_id);
449
+ if (conflict) {
450
+ throw new Error(`Check batch ${conflict} is already active for this target`);
451
+ }
452
+ const sourceAttempts = listCheckAttemptDetails(input.projectId, sourceBatch.execution_run_id);
453
+ if (sourceAttempts.length !== sourceBatch.selected_definition_snapshots.length || sourceAttempts.some(attempt => attempt.outcome === null)) {
454
+ throw new Error('Check rerun source attempt provenance is incomplete');
455
+ }
456
+ const sourceAttemptsByDefinitionId = new Map(sourceAttempts.map(attempt => [attempt.definition_id, attempt]));
457
+ const definitionIds = resolveRerunDefinitionIds(input.selection, sourceAttempts);
458
+ if (definitionIds.length === 0) {
459
+ throw new Error('The source batch has no matching attempts to rerun');
460
+ }
461
+ const availableById = new Map(input.availableDefinitions.map(definition => [definition.id, definition]));
462
+ const selectedDefinitions = definitionIds.map(definitionId => {
463
+ if (!sourceAttemptsByDefinitionId.has(definitionId)) {
464
+ throw new Error(`Check definition ${definitionId} does not belong to the source batch`);
465
+ }
466
+ const definition = availableById.get(definitionId);
467
+ if (!definition) {
468
+ throw new Error(`Check definition ${definitionId} is stale or unavailable`);
469
+ }
470
+ return definition;
471
+ });
472
+ const batchRunId = randomUUID();
473
+ const attempts = selectedDefinitions.map(definition => ({ runId: randomUUID(), definition }));
474
+ const target = sourceBatch.target_kind === 'bundle'
475
+ ? { targetKind: 'bundle', targetBundleId: sourceBatch.target_bundle_id }
476
+ : { targetKind: 'repo_root', targetBundleId: null };
477
+ const scopeLinks = sourceBatch.target_bundle_id
478
+ ? [{ entityType: 'bundle', entityId: sourceBatch.target_bundle_id, relationship: 'context' }]
479
+ : [];
480
+ input.transaction.createExecutionRun({
481
+ id: batchRunId,
482
+ projectId: input.projectId,
483
+ kind: 'check_batch',
484
+ domainStatus: 'queued',
485
+ sourceReference: { kind: 'project', id: input.projectId },
486
+ links: scopeLinks,
487
+ eventType: 'check_batch_rerun_created',
488
+ eventMetadata: { rerun_of_batch_run_id: sourceBatch.execution_run_id },
489
+ checkBatchDetails: {
490
+ ...target,
491
+ targetLabel: sourceBatch.target_label,
492
+ selectedDefinitions,
493
+ initialWorkspaceIdentity: input.initialWorkspaceIdentity,
494
+ rerunOfBatchRunId: sourceBatch.execution_run_id,
495
+ },
496
+ });
497
+ for (const attempt of attempts) {
498
+ input.transaction.createExecutionRun({
499
+ id: attempt.runId,
500
+ projectId: input.projectId,
501
+ kind: 'check_attempt',
502
+ parentRunId: batchRunId,
503
+ domainStatus: 'queued',
504
+ sourceReference: { kind: 'project', id: input.projectId },
505
+ links: scopeLinks,
506
+ eventType: 'check_attempt_rerun_created',
507
+ checkAttemptDetails: {
508
+ batchRunId,
509
+ definition: attempt.definition,
510
+ initialWorkspaceIdentity: input.initialWorkspaceIdentity,
511
+ rerunOfAttemptRunId: sourceAttemptsByDefinitionId.get(attempt.definition.id)?.execution_run_id ?? null,
512
+ },
513
+ });
514
+ }
515
+ input.transaction.transitionExecutionRun({
516
+ projectId: input.projectId,
517
+ runId: batchRunId,
518
+ status: 'running',
519
+ domainStatus: 'running',
520
+ eventType: 'check_batch_started',
521
+ });
522
+ return {
523
+ batchRunId,
524
+ attempts,
525
+ batch: getCheckBatchDetails(input.projectId, batchRunId),
526
+ };
527
+ };
528
+ export const setCheckBatchStopRequested = (projectId, runId) => {
529
+ if (!getCheckBatchDetails(projectId, runId)) {
530
+ throw new Error(`Check batch ${runId} was not found in project ${projectId}`);
531
+ }
532
+ db.prepare('UPDATE check_batch_details SET stop_requested = 1 WHERE execution_run_id = ?').run(runId);
533
+ return getCheckBatchDetails(projectId, runId);
534
+ };
535
+ export const findActiveCheckBatchForTarget = (projectId, targetKind, targetBundleId) => {
536
+ const row = db.prepare(`
537
+ SELECT run.id
538
+ FROM execution_runs run
539
+ JOIN check_batch_details details ON details.execution_run_id = run.id
540
+ WHERE run.project_id = ?
541
+ AND run.status IN ('queued', 'waiting', 'running')
542
+ AND details.target_kind = ?
543
+ AND details.target_bundle_id IS ?
544
+ ORDER BY run.rowid ASC
545
+ LIMIT 1
546
+ `).get(projectId, targetKind, targetBundleId);
547
+ return row?.id ?? null;
548
+ };
549
+ export const hasActiveProjectCheckBatch = (projectId) => Boolean(db.prepare(`
550
+ SELECT 1
551
+ FROM execution_runs run
552
+ JOIN check_batch_details details ON details.execution_run_id = run.id
553
+ WHERE run.project_id = ? AND run.status IN ('queued', 'waiting', 'running')
554
+ LIMIT 1
555
+ `).get(projectId));
556
+ export const updateCheckAttemptOutputState = (input) => {
557
+ const current = getCheckAttemptDetails(input.projectId, input.runId);
558
+ if (!current) {
559
+ throw new Error(`Check attempt ${input.runId} was not found in project ${input.projectId}`);
560
+ }
561
+ if (!Number.isSafeInteger(input.outputRevision)
562
+ || input.outputRevision <= current.output_revision
563
+ || !Number.isSafeInteger(input.retainedStartOffset)
564
+ || input.retainedStartOffset < current.retained_start_offset
565
+ || !Number.isSafeInteger(input.retainedEndOffset)
566
+ || input.retainedEndOffset < input.retainedStartOffset
567
+ || !Number.isSafeInteger(input.totalPersistedBytes)
568
+ || input.totalPersistedBytes < current.total_persisted_bytes
569
+ || input.retainedEndOffset > input.totalPersistedBytes) {
570
+ throw new Error('Check attempt output revisions and byte offsets must advance monotonically');
571
+ }
572
+ db.prepare(`
573
+ UPDATE check_attempt_details
574
+ SET output_revision = ?, retained_start_offset = ?, retained_end_offset = ?,
575
+ total_persisted_bytes = ?, output_retention_state = ?
576
+ WHERE execution_run_id = ?
577
+ `).run(input.outputRevision, input.retainedStartOffset, input.retainedEndOffset, input.totalPersistedBytes, input.outputRetentionState ?? current.output_retention_state, input.runId);
578
+ return getCheckAttemptDetails(input.projectId, input.runId);
579
+ };
580
+ export const appendCheckAttemptOutput = (input) => {
581
+ const append = db.transaction(() => {
582
+ const current = getCheckAttemptDetails(input.projectId, input.runId);
583
+ if (!current) {
584
+ throw new Error(`Check attempt ${input.runId} was not found in project ${input.projectId}`);
585
+ }
586
+ if (current.output_retention_state !== 'retained') {
587
+ throw new Error(`Check attempt ${input.runId} output is not retained`);
588
+ }
589
+ const byteLimit = input.retainedByteLimit ?? CHECK_ATTEMPT_OUTPUT_LIMIT_BYTES;
590
+ if (!Number.isSafeInteger(byteLimit) || byteLimit < 1) {
591
+ throw new Error('Check attempt retained output byte limit must be a positive integer');
592
+ }
593
+ const chunkBytes = Buffer.byteLength(input.chunk, 'utf8');
594
+ const fromOffset = current.total_persisted_bytes;
595
+ const toOffset = fromOffset + chunkBytes;
596
+ if (!Number.isSafeInteger(toOffset)) {
597
+ throw new Error('Check attempt output byte count exceeds the safe integer range');
598
+ }
599
+ const retainedOutput = retainUtf8Suffix(`${current.retained_output}${input.chunk}`, byteLimit);
600
+ const retainedBytes = Buffer.byteLength(retainedOutput, 'utf8');
601
+ const retainedStartOffset = toOffset - retainedBytes;
602
+ const revision = current.output_revision + 1;
603
+ db.prepare(`
604
+ UPDATE check_attempt_details
605
+ SET output_revision = ?, retained_start_offset = ?, retained_end_offset = ?,
606
+ total_persisted_bytes = ?, retained_output = ?
607
+ WHERE execution_run_id = ?
608
+ `).run(revision, retainedStartOffset, toOffset, toOffset, retainedOutput, input.runId);
609
+ return {
610
+ project_id: input.projectId,
611
+ batch_run_id: current.batch_run_id,
612
+ attempt_run_id: input.runId,
613
+ revision,
614
+ from_offset: fromOffset,
615
+ to_offset: toOffset,
616
+ retained_start_offset: retainedStartOffset,
617
+ chunk: input.chunk,
618
+ output_truncated: retainedStartOffset > 0,
619
+ };
620
+ });
621
+ return append.immediate();
622
+ };
623
+ export const completeCheckAttemptDetails = (input) => {
624
+ if (!CHECK_ATTEMPT_OUTCOMES.includes(input.outcome)) {
625
+ throw new Error(`Unsupported check attempt outcome: ${input.outcome}`);
626
+ }
627
+ assertWorkspaceIdentity(input.finalWorkspaceIdentity, 'Final workspace identity');
628
+ const current = getCheckAttemptDetails(input.projectId, input.runId);
629
+ if (!current) {
630
+ throw new Error(`Check attempt ${input.runId} was not found in project ${input.projectId}`);
631
+ }
632
+ if (current.outcome) {
633
+ const agrees = current.outcome === input.outcome
634
+ && JSON.stringify(current.final_workspace_identity) === JSON.stringify(canonicalWorkspaceIdentity(input.finalWorkspaceIdentity))
635
+ && current.exit_code === (input.exitCode ?? null)
636
+ && current.exit_signal === (input.exitSignal ?? null)
637
+ && current.exit_error === (input.exitError ?? null);
638
+ if (!agrees) {
639
+ throw new Error(`Check attempt ${input.runId} already has immutable conflicting outcome details`);
640
+ }
641
+ return current;
642
+ }
643
+ db.prepare(`
644
+ UPDATE check_attempt_details
645
+ SET final_workspace_identity_json = ?, outcome = ?, exit_code = ?, exit_signal = ?, exit_error = ?
646
+ WHERE execution_run_id = ?
647
+ `).run(JSON.stringify(canonicalWorkspaceIdentity(input.finalWorkspaceIdentity)), input.outcome, input.exitCode ?? null, input.exitSignal ?? null, input.exitError ?? null, input.runId);
648
+ return getCheckAttemptDetails(input.projectId, input.runId);
649
+ };
650
+ export const refreshCheckBatchAggregation = (projectId, batchRunId) => {
651
+ const existingBatch = getCheckBatchDetails(projectId, batchRunId);
652
+ if (!existingBatch) {
653
+ throw new Error(`Check batch ${batchRunId} was not found in project ${projectId}`);
654
+ }
655
+ const counts = db.prepare(`
656
+ SELECT
657
+ SUM(CASE WHEN run.status IN ('queued', 'waiting') THEN 1 ELSE 0 END) AS queued_count,
658
+ SUM(CASE WHEN run.status = 'running' THEN 1 ELSE 0 END) AS running_count,
659
+ SUM(CASE WHEN details.outcome = 'passed' THEN 1 ELSE 0 END) AS passed_count,
660
+ SUM(CASE WHEN details.outcome IN ('command_failure', 'spawn_failure', 'timeout', 'workspace_changed_before_start') THEN 1 ELSE 0 END) AS failed_count,
661
+ SUM(CASE WHEN details.outcome = 'user_stop' THEN 1 ELSE 0 END) AS cancelled_count,
662
+ SUM(CASE WHEN details.outcome IN ('restart_loss', 'shutdown_loss') THEN 1 ELSE 0 END) AS interrupted_count
663
+ FROM check_attempt_details details
664
+ JOIN execution_runs run ON run.id = details.execution_run_id
665
+ WHERE details.batch_run_id = ? AND run.project_id = ?
666
+ `).get(batchRunId, projectId);
667
+ db.prepare(`
668
+ UPDATE check_batch_details
669
+ SET queued_count = ?, running_count = ?, passed_count = ?, failed_count = ?,
670
+ cancelled_count = ?, interrupted_count = ?
671
+ WHERE execution_run_id = ?
672
+ `).run(counts.queued_count ?? 0, counts.running_count ?? 0, counts.passed_count ?? 0, counts.failed_count ?? 0, counts.cancelled_count ?? 0, counts.interrupted_count ?? 0, batchRunId);
673
+ return getCheckBatchDetails(projectId, batchRunId);
674
+ };
675
+ export const inspectCheckBatchAggregation = (projectId, batchRunId) => {
676
+ const aggregate = db.transaction(() => {
677
+ const before = getCheckBatchDetails(projectId, batchRunId);
678
+ if (!before) {
679
+ throw new Error(`Check batch ${batchRunId} was not found in project ${projectId}`);
680
+ }
681
+ const attempts = listCheckAttemptDetails(projectId, batchRunId);
682
+ const batch = refreshCheckBatchAggregation(projectId, batchRunId);
683
+ const expectedChildCount = batch.selected_definition_snapshots.length;
684
+ const activeChildCount = attempts.filter(attempt => !TERMINAL_STATUSES.has(attempt.execution_status)).length;
685
+ const terminalWithoutOutcome = attempts.some(attempt => TERMINAL_STATUSES.has(attempt.execution_status) && attempt.outcome === null);
686
+ const activeWithOutcome = attempts.some(attempt => !TERMINAL_STATUSES.has(attempt.execution_status) && attempt.outcome !== null);
687
+ let infrastructureError = null;
688
+ if (attempts.length !== expectedChildCount) {
689
+ infrastructureError = `Check batch expected ${expectedChildCount} children but found ${attempts.length}`;
690
+ }
691
+ else if (terminalWithoutOutcome || activeWithOutcome) {
692
+ infrastructureError = 'Check batch child status and outcome state is inconsistent';
693
+ }
694
+ else if (TERMINAL_STATUSES.has(batch.execution_status) && activeChildCount > 0) {
695
+ infrastructureError = 'Terminal check batch still has active children';
696
+ }
697
+ let terminalStatus = null;
698
+ if (activeChildCount === 0) {
699
+ if (batch.interrupted_count > 0) {
700
+ terminalStatus = 'interrupted';
701
+ }
702
+ else if (batch.stop_requested) {
703
+ terminalStatus = 'cancelled';
704
+ }
705
+ else if (infrastructureError || batch.failed_count > 0) {
706
+ terminalStatus = 'failed';
707
+ }
708
+ else if (batch.cancelled_count > 0) {
709
+ terminalStatus = 'cancelled';
710
+ }
711
+ else if (batch.passed_count === expectedChildCount) {
712
+ terminalStatus = 'completed';
713
+ }
714
+ else {
715
+ infrastructureError = infrastructureError ?? 'Check batch children did not produce a complete terminal outcome set';
716
+ terminalStatus = 'failed';
717
+ }
718
+ }
719
+ return {
720
+ batch,
721
+ child_count: attempts.length,
722
+ expected_child_count: expectedChildCount,
723
+ active_child_count: activeChildCount,
724
+ terminal_status: terminalStatus,
725
+ infrastructure_error: infrastructureError,
726
+ };
727
+ });
728
+ return aggregate();
729
+ };
730
+ export const isCheckAttemptCurrent = (details, workspaceIdentity) => (details.outcome === 'passed'
731
+ && details.initial_workspace_identity.state === 'available'
732
+ && details.final_workspace_identity?.state === 'available'
733
+ && workspaceIdentity.state === 'available'
734
+ && details.initial_workspace_identity.fingerprint === details.final_workspace_identity.fingerprint
735
+ && details.final_workspace_identity.fingerprint === workspaceIdentity.fingerprint);
736
+ //# sourceMappingURL=checkRuns.js.map