@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.
- package/dist/client/assets/ActivityView-DwfDSF4q.js +1 -0
- package/dist/client/assets/GitHubActionsDialog-3dMCYCSZ.js +1 -0
- package/dist/client/assets/VerificationDialog-CoTfXMyb.js +4 -0
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-D0PXGvSL.js → architectureDiagram-3BPJPVTR-Bdnc97XK.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-BCLAxnp5.js → blockDiagram-GPEHLZMM-boxM3She.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-DVZ5yZWV.js → c4Diagram-AAUBKEIU-CJDdF7xd.js} +1 -1
- package/dist/client/assets/channel-BGqdAcIT.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-BGJYj1Lu.js → chunk-2J33WTMH-ClpcO-Om.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-DI5-yK3m.js → chunk-3OPIFGDE-CcuL_65J.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-Dnr5DDsb.js → chunk-5ZQYHXKU-CUGYZXkH.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-8HdFwn-P.js → chunk-727SXJPM-CApeLDyK.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-C7x9t2uJ.js → chunk-AQP2D5EJ-8FdSvF2D.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-CrhwRPh5.js → chunk-CSCIHK7Q-CZoIOF3a.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-CdD62r5U.js → chunk-KSCS5N6A-BigFQI5y.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-CqQVvwc_.js → chunk-L5ZTLDWV-DIi3SHaT.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-DLHrqoJ4.js → chunk-LZXEDZCA-B-Z0BcQy.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-B0yOdiPF.js → chunk-ND2GUHAM-EBeba0mg.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-boM-MocY.js → chunk-NZK2D7GU-DJBI2y53.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-B0jgJn0t.js → chunk-O5CBEL6O-BqxdXhMd.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-C_S_v-m2.js → chunk-WU5MYG2G-Coe1_Cbb.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-D5VuKRcj.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B9GNqpRr.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-DKl6Y8ya.js → dagre-BM42HDAG-tuTmeA0Y.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-6Lyn9DBp.js → diagram-2AECGRRQ-ILGCpG2F.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-wdS1vEkN.js → diagram-5GNKFQAL-C8qBZsgK.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-CPih29jF.js → diagram-KO2AKTUF-BXlcm_Zo.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-Cr5Px74d.js → diagram-LMA3HP47-B8V5EVbD.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-i0PjSScd.js → diagram-OG6HWLK6-CWsdOqcP.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-Dt4yOC9J.js → erDiagram-TEJ5UH35-DW8pgly2.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-Bf6kvajC.js → flowDiagram-I6XJVG4X-Djn7fOVR.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-DgxePMk_.js → ganttDiagram-6RSMTGT7-DZ421MEy.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BZq2cmFS.js → gitGraphDiagram-PVQCEYII-CCNzLPuP.js} +1 -1
- package/dist/client/assets/index-BRc02lbW.js +273 -0
- package/dist/client/assets/index-CZO9HL1Z.css +2 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-Chij69KN.js → infoDiagram-5YYISTIA-Bd1gYgMp.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-3qHCLFgD.js → ishikawaDiagram-YF4QCWOH-txyMoatJ.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-DvRW8tbA.js → journeyDiagram-JHISSGLW-DhT3bmX6.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-DjR2T5I1.js → kanban-definition-UN3LZRKU-BBHt2hfC.js} +1 -1
- package/dist/client/assets/{line-B9OzrQ1Y.js → line-KGRcT7Qz.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-DYcDor4d.js → mermaid-parser.core-CDzt4_zR.js} +1 -1
- package/dist/client/assets/{mermaid.core-DG8QYo2u.js → mermaid.core-DD_-k1aB.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-f4QPF6b3.js → mindmap-definition-RKZ34NQL-DoctVTDl.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-CMkl2jBu.js → pieDiagram-4H26LBE5-DFlLEi70.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-BXaUHzJi.js → quadrantDiagram-W4KKPZXB-3YHOQgl7.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-B-Inuj_6.js → requirementDiagram-4Y6WPE33-X4fZVL9M.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BjsmZFYI.js → sankeyDiagram-5OEKKPKP-BUPnMXeG.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-B1PAm6sX.js → sequenceDiagram-3UESZ5HK-D6QU3M64.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-pLONNEN8.js → stateDiagram-AJRCARHV-c35bu4qZ.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ymy1_EHr.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-BLMkSXo_.js → timeline-definition-PNZ67QCA-CX2y6e_b.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-CEwGRegG.js → vennDiagram-CIIHVFJN-BDaWltjF.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-_md2IBXC.js → wardleyDiagram-YWT4CUSO-BpBRL2sb.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-DVzwfXHc.js → xychartDiagram-2RQKCTM6-Bqw7X0UT.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +460 -2
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +389 -47
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/activeAutomationChild.js +31 -0
- package/dist/server/lib/activeAutomationChild.js.map +1 -0
- package/dist/server/lib/automationCheckGate.js +266 -0
- package/dist/server/lib/automationCheckGate.js.map +1 -0
- package/dist/server/lib/automationProgression.js +20 -21
- package/dist/server/lib/automationProgression.js.map +1 -1
- package/dist/server/lib/automationReconciliation.js +16 -2
- package/dist/server/lib/automationReconciliation.js.map +1 -1
- package/dist/server/lib/automationResumeDecision.js +4 -1
- package/dist/server/lib/automationResumeDecision.js.map +1 -1
- package/dist/server/lib/automationStageDetails.js +173 -41
- package/dist/server/lib/automationStageDetails.js.map +1 -1
- package/dist/server/lib/chatMessages.js +37 -8
- package/dist/server/lib/chatMessages.js.map +1 -1
- package/dist/server/lib/chatOperations.js +119 -68
- package/dist/server/lib/chatOperations.js.map +1 -1
- package/dist/server/lib/checkDefinitions.js +278 -0
- package/dist/server/lib/checkDefinitions.js.map +1 -0
- package/dist/server/lib/checkGateEvaluations.js +294 -0
- package/dist/server/lib/checkGateEvaluations.js.map +1 -0
- package/dist/server/lib/checkRunner.js +674 -0
- package/dist/server/lib/checkRunner.js.map +1 -0
- package/dist/server/lib/checkRuns.js +736 -0
- package/dist/server/lib/checkRuns.js.map +1 -0
- package/dist/server/lib/checkWorkspaceIdentity.js +207 -0
- package/dist/server/lib/checkWorkspaceIdentity.js.map +1 -0
- package/dist/server/lib/executionReconciliation.js +79 -2
- package/dist/server/lib/executionReconciliation.js.map +1 -1
- package/dist/server/lib/executionRetry.js +14 -6
- package/dist/server/lib/executionRetry.js.map +1 -1
- package/dist/server/lib/executionRetryContext.js +3 -3
- package/dist/server/lib/executionRetryContext.js.map +1 -1
- package/dist/server/lib/executionRunRetention.js +41 -3
- package/dist/server/lib/executionRunRetention.js.map +1 -1
- package/dist/server/lib/executionRuns.js +135 -2
- package/dist/server/lib/executionRuns.js.map +1 -1
- package/dist/server/lib/executionTimeline.js +52 -1
- package/dist/server/lib/executionTimeline.js.map +1 -1
- package/dist/server/lib/mobileAccess.js +44 -6
- package/dist/server/lib/mobileAccess.js.map +1 -1
- package/dist/server/lib/planModeQuestions.js +12 -7
- package/dist/server/lib/planModeQuestions.js.map +1 -1
- package/dist/server/lib/planQuestionOperations.js +246 -0
- package/dist/server/lib/planQuestionOperations.js.map +1 -0
- package/dist/server/lib/planQuestions.js +139 -0
- package/dist/server/lib/planQuestions.js.map +1 -0
- package/dist/server/lib/projectAutoConfig.js +47 -65
- package/dist/server/lib/projectAutoConfig.js.map +1 -1
- package/dist/server/lib/projects.js +87 -7
- package/dist/server/lib/projects.js.map +1 -1
- package/dist/server/lib/realtime.js +24 -1
- package/dist/server/lib/realtime.js.map +1 -1
- package/dist/server/lib/requiredCheckPolicy.js +68 -0
- package/dist/server/lib/requiredCheckPolicy.js.map +1 -0
- package/dist/server/lib/shutdown.js +4 -4
- package/dist/server/lib/shutdown.js.map +1 -1
- package/dist/server/lib/specArtifact.js +36 -38
- package/dist/server/lib/specArtifact.js.map +1 -1
- package/dist/server/lib/specMessages.js +31 -18
- package/dist/server/lib/specMessages.js.map +1 -1
- package/dist/server/lib/specOperations.js +129 -78
- package/dist/server/lib/specOperations.js.map +1 -1
- package/dist/server/lib/testCommandRunner.js +3 -2
- package/dist/server/lib/testCommandRunner.js.map +1 -1
- package/dist/server/lib/testSessions.js +2 -6
- package/dist/server/lib/testSessions.js.map +1 -1
- package/dist/server/lib/ticketAutomationCommit.js +185 -22
- package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
- package/dist/server/lib/ticketFollowUpOperations.js +19 -6
- package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
- package/dist/server/lib/ticketMessages.js +38 -9
- package/dist/server/lib/ticketMessages.js.map +1 -1
- package/dist/server/lib/ticketWorkflowOperations.js +10 -1
- package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
- package/dist/server/lib/tickets.js +21 -1
- package/dist/server/lib/tickets.js.map +1 -1
- package/dist/server/lib/types.js +2 -0
- package/dist/server/lib/types.js.map +1 -1
- package/dist/server/workers/chat.js +48 -2
- package/dist/server/workers/chat.js.map +1 -1
- package/dist/server/workers/followUp.js +50 -2
- package/dist/server/workers/followUp.js.map +1 -1
- package/dist/server/workers/plan.js +57 -4
- package/dist/server/workers/plan.js.map +1 -1
- package/dist/server/workers/planFollowUp.js +51 -2
- package/dist/server/workers/planFollowUp.js.map +1 -1
- package/dist/server/workers/review.js +2 -10
- package/dist/server/workers/review.js.map +1 -1
- package/dist/server/workers/specSession.js +55 -5
- package/dist/server/workers/specSession.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/ActivityView-BrZY8vYf.js +0 -1
- package/dist/client/assets/GitHubActionsDialog-CUfdVw97.js +0 -1
- package/dist/client/assets/TestsDialog-FXcnLOyf.js +0 -4
- package/dist/client/assets/channel-BCPCrThZ.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BdJlc_1i.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Bidw5Zc1.js +0 -1
- package/dist/client/assets/index-BJGHMaMz.css +0 -2
- package/dist/client/assets/index-CjRMYO1U.js +0 -273
- 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
|