@cassiomc1/forgeloop 0.1.2 → 0.1.3
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/.cursor/rules/project-loop.mdc +5 -0
- package/.github/copilot-instructions.md +5 -0
- package/AGENTS.md +5 -1
- package/CLAUDE.md +5 -1
- package/LOOP_ENGINEERING.md +30 -6
- package/QUALITY_SCORECARD.md +12 -0
- package/README.md +21 -0
- package/conformance/README.md +8 -3
- package/package.json +1 -1
- package/src/cli.js +25 -6
- package/src/commands/next.js +29 -0
- package/src/core/completion-artifacts.js +49 -9
- package/src/core/completion-relationships.js +141 -0
- package/src/core/completion.js +33 -62
- package/src/core/events.js +34 -0
- package/src/core/execution-prerequisites.js +200 -0
- package/src/core/next-action.js +709 -0
- package/src/core/phase.js +142 -18
- package/src/core/preflight.js +165 -2
package/src/core/completion.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact } from "./artifacts.js";
|
|
1
|
+
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
2
2
|
import { requiredEvidenceForTarget } from "./completion-artifacts.js";
|
|
3
|
-
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
3
|
+
import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger } from "./events.js";
|
|
4
4
|
import { evaluatePreflight } from "./preflight.js";
|
|
5
5
|
import { readContract } from "./contract.js";
|
|
6
6
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
7
7
|
import { readWorkState, writeWorkState, classifyLoadedWorkState } from "./work-state.js";
|
|
8
|
-
import { validateReceipt } from "./receipt.js";
|
|
9
|
-
import {
|
|
10
|
-
import { assertCoverageList, coverageForRequirements } from "./coverage.js";
|
|
8
|
+
import { createReceipt, validateReceipt } from "./receipt.js";
|
|
9
|
+
import { completionRelationshipErrors } from "./completion-relationships.js";
|
|
11
10
|
import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
|
|
11
|
+
import { evaluateStartExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
|
|
12
12
|
|
|
13
13
|
function issue(code, message, artifacts = [], details = {}) {
|
|
14
14
|
return { code, message, artifacts, ...details };
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export function completionIdentityErrors({ contract, state, receipt } = {}) {
|
|
18
|
+
return completionRelationshipErrors({ contract, state, receipt })
|
|
19
|
+
.filter((error) => ["E_STATE_TASK_MISMATCH", "E_RECEIPT_TASK_MISMATCH"].includes(error.code));
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
function repairNext(error) {
|
|
18
23
|
switch (error.code) {
|
|
19
24
|
case "E_RECEIPT_MISSING":
|
|
@@ -94,7 +99,7 @@ async function validateLedger(target, taskId, state, errors, packageRoot) {
|
|
|
94
99
|
}
|
|
95
100
|
const ledger = await validateEventLedger(target, packageRoot);
|
|
96
101
|
for (const error of ledger.errors) errors.push({ ...error, artifacts: [ARTIFACT_PATHS.events] });
|
|
97
|
-
const requiredEvents =
|
|
102
|
+
const requiredEvents = LIFECYCLE_MILESTONES.slice(0, state?.phase === "COMPLETE" ? undefined : -1);
|
|
98
103
|
const observed = new Set(ledger.events.filter((event) => event.taskId === taskId).map((event) => event.event));
|
|
99
104
|
for (const event of requiredEvents) {
|
|
100
105
|
if (!observed.has(event)) {
|
|
@@ -104,8 +109,8 @@ async function validateLedger(target, taskId, state, errors, packageRoot) {
|
|
|
104
109
|
if (ledger.events.some((event) => event.taskId !== taskId)) {
|
|
105
110
|
errors.push(issue("E_PHASE_CHRONOLOGY_INVALID", "Protocol events must belong to the current task", [ARTIFACT_PATHS.events]));
|
|
106
111
|
}
|
|
107
|
-
if (state?.phase
|
|
108
|
-
errors.push(issue("E_PHASE_CHRONOLOGY_INVALID", "COMPLETE state
|
|
112
|
+
if (state?.phase !== "COMPLETE" && observed.has("COMPLETION_VALIDATED")) {
|
|
113
|
+
errors.push(issue("E_PHASE_CHRONOLOGY_INVALID", "COMPLETION_VALIDATED requires COMPLETE state", [ARTIFACT_PATHS.events]));
|
|
109
114
|
}
|
|
110
115
|
return ledger;
|
|
111
116
|
}
|
|
@@ -150,6 +155,11 @@ export async function evaluateCompletion({ target, packageRoot, strict = false }
|
|
|
150
155
|
errors,
|
|
151
156
|
);
|
|
152
157
|
|
|
158
|
+
if (state && hasExecutionStarted(state.phase)) {
|
|
159
|
+
const prerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot });
|
|
160
|
+
errors.push(...prerequisites.errors);
|
|
161
|
+
}
|
|
162
|
+
|
|
153
163
|
if (receipt) {
|
|
154
164
|
try {
|
|
155
165
|
await validateReceipt(receipt.value, packageRoot);
|
|
@@ -158,34 +168,12 @@ export async function evaluateCompletion({ target, packageRoot, strict = false }
|
|
|
158
168
|
}
|
|
159
169
|
}
|
|
160
170
|
|
|
161
|
-
if (contract && route && route.value.contractFingerprint !== undefined
|
|
162
|
-
&& route.value.contractFingerprint !== contract.fingerprint) {
|
|
163
|
-
errors.push(issue("E_ROUTE_STALE", "Routing result does not match the current contract", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.contract]));
|
|
164
|
-
}
|
|
165
|
-
if (contract && state && state.contractFingerprint !== contract.fingerprint) {
|
|
166
|
-
errors.push(issue("E_CONTRACT_STALE", "Work state does not match the current contract", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract]));
|
|
167
|
-
}
|
|
168
|
-
if (route && state && JSON.stringify(route.value.guides) !== JSON.stringify(state.selectedGuides)) {
|
|
169
|
-
errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "Work state guides do not match the persisted route", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.state]));
|
|
170
|
-
}
|
|
171
|
-
if (route && receipt && JSON.stringify(route.value.guides) !== JSON.stringify(receipt.value.selectedGuides)) {
|
|
172
|
-
errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "Receipt guides do not match the persisted route", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.receipt]));
|
|
173
|
-
}
|
|
174
|
-
if (contract && receipt && receipt.value.contractFingerprint !== contract.fingerprint) {
|
|
175
|
-
errors.push(issue("E_RECEIPT_CONTRACT_MISMATCH", "Receipt does not match the current contract", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.receipt]));
|
|
176
|
-
}
|
|
177
|
-
if (route && receipt && receipt.value.routeFingerprint !== undefined && receipt.value.routeFingerprint !== route.fingerprint) {
|
|
178
|
-
errors.push(issue("E_RECEIPT_ROUTE_MISMATCH", "Receipt does not match the persisted route", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.receipt]));
|
|
179
|
-
}
|
|
180
|
-
if (state && receipt && receipt.value.stateFingerprint !== undefined && receipt.value.stateFingerprint !== canonicalFingerprint(state)) {
|
|
181
|
-
errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Receipt does not match the recorded work state", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
|
|
182
|
-
}
|
|
183
171
|
if (state && !["REVIEWING", "COMPLETE"].includes(state.phase)) {
|
|
184
172
|
errors.push(issue("E_PHASE_PREREQUISITE_MISSING", `Completion requires REVIEWING or COMPLETE state, found ${state.phase}`, [ARTIFACT_PATHS.state]));
|
|
185
173
|
}
|
|
186
174
|
|
|
187
175
|
let coverage = [];
|
|
188
|
-
if (
|
|
176
|
+
if (contract && route) {
|
|
189
177
|
const requiredEvidence = await requiredEvidenceForTarget({
|
|
190
178
|
target,
|
|
191
179
|
contract,
|
|
@@ -193,37 +181,15 @@ export async function evaluateCompletion({ target, packageRoot, strict = false }
|
|
|
193
181
|
packageRoot,
|
|
194
182
|
additionalEvidence: preflight?.policy?.requiredEvidence ?? [],
|
|
195
183
|
});
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
for (const requirement of requiredEvidence) {
|
|
206
|
-
const item = byRequirement.get(requirement);
|
|
207
|
-
if (!item) {
|
|
208
|
-
errors.push(issue("E_EVIDENCE_REQUIRED", `Evidence coverage is missing: ${requirement}`, [ARTIFACT_PATHS.receipt], { requirement }));
|
|
209
|
-
} else if (item.status !== "COVERED") {
|
|
210
|
-
errors.push(issue(
|
|
211
|
-
item.status === "BLOCKED" ? "E_EVIDENCE_COVERAGE_PARTIAL" : "E_EVIDENCE_COVERAGE_PARTIAL",
|
|
212
|
-
`Evidence coverage is ${item.status}: ${requirement}`,
|
|
213
|
-
[ARTIFACT_PATHS.receipt],
|
|
214
|
-
{ requirement, status: item.status },
|
|
215
|
-
));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
const structuredChecks = receipt.value.checks.filter((check) => check?.schemaVersion === 1 || check?.id !== undefined || check?.evidenceKind !== undefined);
|
|
219
|
-
if (structuredChecks.length > 0) {
|
|
220
|
-
try {
|
|
221
|
-
assertCheckList(structuredChecks, "receipt.checks");
|
|
222
|
-
errors.push(...requiredChecksSatisfied(structuredChecks, requiredEvidence));
|
|
223
|
-
} catch (error) {
|
|
224
|
-
errors.push(issue(error.code ?? "E_CHECK_INVALID", error.message, [ARTIFACT_PATHS.receipt]));
|
|
225
|
-
}
|
|
226
|
-
}
|
|
184
|
+
const relationshipErrors = completionRelationshipErrors({
|
|
185
|
+
contract,
|
|
186
|
+
route,
|
|
187
|
+
state,
|
|
188
|
+
receipt: receipt?.value,
|
|
189
|
+
requiredEvidence,
|
|
190
|
+
});
|
|
191
|
+
errors.push(...relationshipErrors);
|
|
192
|
+
coverage = receipt?.value?.evidenceCoverage ?? [];
|
|
227
193
|
}
|
|
228
194
|
|
|
229
195
|
const ledger = contract && state
|
|
@@ -283,7 +249,12 @@ export async function runComplete({ target, packageRoot, strict = false, persist
|
|
|
283
249
|
publicationStatus: result.publicationStatus,
|
|
284
250
|
lastUpdated: new Date().toISOString(),
|
|
285
251
|
};
|
|
252
|
+
const nextReceipt = await createReceipt({
|
|
253
|
+
...receipt.value,
|
|
254
|
+
stateFingerprint: canonicalFingerprint(next),
|
|
255
|
+
}, packageRoot);
|
|
286
256
|
await writeWorkState(target, next, { packageRoot });
|
|
257
|
+
await writeJsonArtifact(target, ARTIFACT_PATHS.receipt, nextReceipt, "execution-receipt", packageRoot);
|
|
287
258
|
}
|
|
288
259
|
const contract = await readContract(target, packageRoot);
|
|
289
260
|
const ledger = await validateEventLedger(target, packageRoot);
|
package/src/core/events.js
CHANGED
|
@@ -10,6 +10,16 @@ import { assertSecretFree } from "./receipt.js";
|
|
|
10
10
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
11
11
|
|
|
12
12
|
const EVENT_SCHEMA_VERSION = 1;
|
|
13
|
+
export const LIFECYCLE_MILESTONES = Object.freeze([
|
|
14
|
+
"CONTRACT_VALIDATED",
|
|
15
|
+
"ROUTE_VALIDATED",
|
|
16
|
+
"PREFLIGHT_READY",
|
|
17
|
+
"EXECUTION_STARTED",
|
|
18
|
+
"VERIFICATION_STARTED",
|
|
19
|
+
"VERIFICATION_RECORDED",
|
|
20
|
+
"COMPLETION_VALIDATED",
|
|
21
|
+
]);
|
|
22
|
+
const REPEATABLE_MILESTONES = new Set(["VERIFICATION_RECORDED"]);
|
|
13
23
|
|
|
14
24
|
function eventHash(event) {
|
|
15
25
|
const { hash, ...body } = event;
|
|
@@ -82,6 +92,8 @@ export async function validateEventLedger(target, packageRoot) {
|
|
|
82
92
|
const errors = [];
|
|
83
93
|
let taskId = null;
|
|
84
94
|
const seen = new Set();
|
|
95
|
+
let lastMilestone = -1;
|
|
96
|
+
const milestoneCounts = new Map();
|
|
85
97
|
for (const [index, event] of events.entries()) {
|
|
86
98
|
if (event.seq !== index + 1) {
|
|
87
99
|
errors.push({ code: "E_EVENT_INVALID", message: `event sequence must be ${index + 1}` });
|
|
@@ -94,6 +106,25 @@ export async function validateEventLedger(target, packageRoot) {
|
|
|
94
106
|
if (event.hash !== eventHash(event)) {
|
|
95
107
|
errors.push({ code: "E_LEDGER_HASH_INVALID", message: `event ${event.seq} hash does not match its content` });
|
|
96
108
|
}
|
|
109
|
+
const milestoneIndex = LIFECYCLE_MILESTONES.indexOf(event.event);
|
|
110
|
+
if (milestoneIndex >= 0) {
|
|
111
|
+
const count = (milestoneCounts.get(event.event) ?? 0) + 1;
|
|
112
|
+
milestoneCounts.set(event.event, count);
|
|
113
|
+
if (count > 1 && !REPEATABLE_MILESTONES.has(event.event)) {
|
|
114
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
|
|
115
|
+
}
|
|
116
|
+
if (milestoneIndex > lastMilestone + 1) {
|
|
117
|
+
errors.push({
|
|
118
|
+
code: "E_PHASE_CHRONOLOGY_INVALID",
|
|
119
|
+
message: `${event.event} is missing prerequisite milestone: ${LIFECYCLE_MILESTONES[lastMilestone + 1]}`,
|
|
120
|
+
});
|
|
121
|
+
} else if (milestoneIndex < lastMilestone) {
|
|
122
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `${event.event} is out of lifecycle order` });
|
|
123
|
+
} else if (milestoneIndex === lastMilestone && !REPEATABLE_MILESTONES.has(event.event)) {
|
|
124
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
|
|
125
|
+
}
|
|
126
|
+
if (milestoneIndex > lastMilestone) lastMilestone = milestoneIndex;
|
|
127
|
+
}
|
|
97
128
|
seen.add(event.event);
|
|
98
129
|
if (event.event === "EXECUTION_STARTED" && !seen.has("ROUTE_VALIDATED")) {
|
|
99
130
|
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before route validation" });
|
|
@@ -116,6 +147,9 @@ export async function validateEventLedger(target, packageRoot) {
|
|
|
116
147
|
}
|
|
117
148
|
}
|
|
118
149
|
}
|
|
150
|
+
if (event.event === "VERIFICATION_RECORDED" && !seen.has("VERIFICATION_STARTED")) {
|
|
151
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "verification evidence recorded before verification started" });
|
|
152
|
+
}
|
|
119
153
|
if (event.event === "COMPLETION_VALIDATED" && !seen.has("VERIFICATION_RECORDED")) {
|
|
120
154
|
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "completion validated before verification evidence" });
|
|
121
155
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { ARTIFACT_PATHS, readJsonArtifact } from "./artifacts.js";
|
|
2
|
+
import { readContract } from "./contract.js";
|
|
3
|
+
import { validateEventLedger } from "./events.js";
|
|
4
|
+
import { evaluatePreflight, validatePersistedPreflight } from "./preflight.js";
|
|
5
|
+
import { readPersistedRoute } from "./route-artifact.js";
|
|
6
|
+
import { stateIdentityErrors } from "./completion-relationships.js";
|
|
7
|
+
import { classifyLoadedWorkState } from "./work-state.js";
|
|
8
|
+
|
|
9
|
+
const START_EXECUTION_EVENTS = Object.freeze([
|
|
10
|
+
"CONTRACT_VALIDATED",
|
|
11
|
+
"ROUTE_VALIDATED",
|
|
12
|
+
"PREFLIGHT_READY",
|
|
13
|
+
]);
|
|
14
|
+
const POST_EXECUTION_PHASES = new Set([
|
|
15
|
+
"EXECUTING",
|
|
16
|
+
"VERIFYING",
|
|
17
|
+
"DIAGNOSING",
|
|
18
|
+
"CORRECTING",
|
|
19
|
+
"REVIEWING",
|
|
20
|
+
"COMPLETE",
|
|
21
|
+
]);
|
|
22
|
+
export const PREFLIGHT_ROUTE_IDENTITY_ERROR_MESSAGE = "PREFLIGHT_READY event routing fingerprint does not match the current READY preflight and route";
|
|
23
|
+
export const PREFLIGHT_CONTRACT_IDENTITY_ERROR_MESSAGE = "PREFLIGHT_READY event contract fingerprint does not match the current READY preflight";
|
|
24
|
+
|
|
25
|
+
export function hasExecutionStarted(phase) {
|
|
26
|
+
return POST_EXECUTION_PHASES.has(phase);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function prerequisiteError(prerequisites) {
|
|
30
|
+
const first = prerequisites.errors[0];
|
|
31
|
+
if (!first) return null;
|
|
32
|
+
const error = new Error(first.message);
|
|
33
|
+
error.code = first.code;
|
|
34
|
+
error.artifacts = first.artifacts;
|
|
35
|
+
return error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function assertExecutionPrerequisites(input = {}) {
|
|
39
|
+
const prerequisites = await evaluateStartExecutionPrerequisites(input);
|
|
40
|
+
const error = prerequisiteError(prerequisites);
|
|
41
|
+
if (error) throw error;
|
|
42
|
+
return prerequisites;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function issue(code, message, artifacts = []) {
|
|
46
|
+
return { code, message, artifacts };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sameStringSet(left, right) {
|
|
50
|
+
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
51
|
+
return left.length === new Set(left).size
|
|
52
|
+
&& right.length === new Set(right).size
|
|
53
|
+
&& left.length === right.length
|
|
54
|
+
&& left.every((value) => right.includes(value));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function load(loadArtifact, code, message, artifacts, errors) {
|
|
58
|
+
try {
|
|
59
|
+
return await loadArtifact();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
errors.push(issue(code, `${message}: ${error.message}`, artifacts));
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function freshnessErrors(state, freshness) {
|
|
67
|
+
if (freshness.status !== "REVALIDATION_REQUIRED") return [];
|
|
68
|
+
return [issue(
|
|
69
|
+
"E_STATE_REVALIDATION_REQUIRED",
|
|
70
|
+
`EXECUTING requires a fresh work-state checkpoint: ${freshness.reasons.join(", ")}`,
|
|
71
|
+
[
|
|
72
|
+
ARTIFACT_PATHS.state,
|
|
73
|
+
ARTIFACT_PATHS.contract,
|
|
74
|
+
...(state.requiredArtifacts?.map((artifact) => artifact.path) ?? []),
|
|
75
|
+
],
|
|
76
|
+
)];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function prerequisiteLedgerErrors(ledger, taskId, preflight, route) {
|
|
80
|
+
const errors = (ledger.errors ?? []).map((error) => issue(
|
|
81
|
+
error.code ?? "E_PHASE_CHRONOLOGY_INVALID",
|
|
82
|
+
error.message,
|
|
83
|
+
[ARTIFACT_PATHS.events],
|
|
84
|
+
));
|
|
85
|
+
const events = ledger.events ?? [];
|
|
86
|
+
if (events.some((event) => event.taskId !== taskId)) {
|
|
87
|
+
errors.push(issue(
|
|
88
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
89
|
+
"EXECUTING requires protocol prerequisite events to belong to the current task",
|
|
90
|
+
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract],
|
|
91
|
+
));
|
|
92
|
+
return errors;
|
|
93
|
+
}
|
|
94
|
+
const currentEvents = events.filter((event) => event.taskId === taskId);
|
|
95
|
+
for (const requiredEvent of START_EXECUTION_EVENTS) {
|
|
96
|
+
if (!currentEvents.some((event) => event.event === requiredEvent)) {
|
|
97
|
+
errors.push(issue(
|
|
98
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
99
|
+
`EXECUTING requires a ${requiredEvent} protocol event`,
|
|
100
|
+
[ARTIFACT_PATHS.events],
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const preflightEvent = currentEvents.find((event) => event.event === "PREFLIGHT_READY");
|
|
105
|
+
if (preflightEvent && (!sameStringSet(preflightEvent.details?.requiredGates, preflight.requiredGates)
|
|
106
|
+
|| !sameStringSet(preflightEvent.details?.satisfiedGates, preflight.satisfiedGates))) {
|
|
107
|
+
errors.push(issue(
|
|
108
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
109
|
+
"PREFLIGHT_READY event gate sets do not match the current READY preflight",
|
|
110
|
+
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.preflight],
|
|
111
|
+
));
|
|
112
|
+
}
|
|
113
|
+
if (preflightEvent && preflightEvent.fingerprint !== preflight.fingerprints.contract) {
|
|
114
|
+
errors.push(issue(
|
|
115
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
116
|
+
PREFLIGHT_CONTRACT_IDENTITY_ERROR_MESSAGE,
|
|
117
|
+
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.contract],
|
|
118
|
+
));
|
|
119
|
+
}
|
|
120
|
+
if (preflightEvent && (preflightEvent.details?.routingFingerprint !== preflight.fingerprints.routing
|
|
121
|
+
|| preflightEvent.details?.routingFingerprint !== route.fingerprint)) {
|
|
122
|
+
errors.push(issue(
|
|
123
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
124
|
+
PREFLIGHT_ROUTE_IDENTITY_ERROR_MESSAGE,
|
|
125
|
+
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.route],
|
|
126
|
+
));
|
|
127
|
+
}
|
|
128
|
+
return errors;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function evaluateStartExecutionPrerequisites({ target, state, packageRoot } = {}) {
|
|
132
|
+
const errors = [];
|
|
133
|
+
const requiredArtifacts = [
|
|
134
|
+
ARTIFACT_PATHS.state,
|
|
135
|
+
ARTIFACT_PATHS.contract,
|
|
136
|
+
ARTIFACT_PATHS.route,
|
|
137
|
+
ARTIFACT_PATHS.preflight,
|
|
138
|
+
ARTIFACT_PATHS.events,
|
|
139
|
+
];
|
|
140
|
+
if (!state) {
|
|
141
|
+
return {
|
|
142
|
+
errors: [issue("E_PHASE_PREREQUISITE_MISSING", "EXECUTING requires a work state", [ARTIFACT_PATHS.state])],
|
|
143
|
+
requiredArtifacts,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const contract = await load(
|
|
148
|
+
() => readContract(target, packageRoot),
|
|
149
|
+
"E_PHASE_PREREQUISITE_MISSING",
|
|
150
|
+
`EXECUTING requires ${ARTIFACT_PATHS.contract}`,
|
|
151
|
+
[ARTIFACT_PATHS.contract],
|
|
152
|
+
errors,
|
|
153
|
+
);
|
|
154
|
+
const route = await load(
|
|
155
|
+
() => readPersistedRoute(target, packageRoot),
|
|
156
|
+
"E_PHASE_PREREQUISITE_MISSING",
|
|
157
|
+
`EXECUTING requires ${ARTIFACT_PATHS.route}`,
|
|
158
|
+
[ARTIFACT_PATHS.route],
|
|
159
|
+
errors,
|
|
160
|
+
);
|
|
161
|
+
if (!contract || !route) return { errors, requiredArtifacts, contract, route };
|
|
162
|
+
|
|
163
|
+
if (state.routeFingerprint !== route.fingerprint) {
|
|
164
|
+
errors.push(issue(
|
|
165
|
+
"E_ROUTE_STALE",
|
|
166
|
+
"EXECUTING requires work state and route to match the current contract",
|
|
167
|
+
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.route, ARTIFACT_PATHS.contract],
|
|
168
|
+
));
|
|
169
|
+
}
|
|
170
|
+
errors.push(...stateIdentityErrors({ contract, route, state }));
|
|
171
|
+
|
|
172
|
+
const freshness = await classifyLoadedWorkState({
|
|
173
|
+
target,
|
|
174
|
+
state,
|
|
175
|
+
contractFile: ARTIFACT_PATHS.contract,
|
|
176
|
+
});
|
|
177
|
+
errors.push(...freshnessErrors(state, freshness));
|
|
178
|
+
|
|
179
|
+
const preflight = await evaluatePreflight({ target, packageRoot });
|
|
180
|
+
let persistedPreflight = null;
|
|
181
|
+
try {
|
|
182
|
+
persistedPreflight = await readJsonArtifact(target, ARTIFACT_PATHS.preflight, "preflight", packageRoot);
|
|
183
|
+
} catch {
|
|
184
|
+
// validatePersistedPreflight reports the stable, actionable preflight reason.
|
|
185
|
+
}
|
|
186
|
+
const persistedPreflightErrors = validatePersistedPreflight(persistedPreflight?.value, preflight);
|
|
187
|
+
if (!sameStringSet(state.requiredGates, preflight.requiredGates)
|
|
188
|
+
|| !sameStringSet(state.satisfiedGates, preflight.satisfiedGates)) {
|
|
189
|
+
errors.push(issue(
|
|
190
|
+
"E_PREFLIGHT_GATES_STALE",
|
|
191
|
+
"Work state gate sets do not match the current preflight evaluation",
|
|
192
|
+
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.gates],
|
|
193
|
+
));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const ledger = await validateEventLedger(target, packageRoot);
|
|
197
|
+
errors.push(...prerequisiteLedgerErrors(ledger, contract.value.taskId, preflight, route));
|
|
198
|
+
errors.push(...persistedPreflightErrors);
|
|
199
|
+
return { errors, requiredArtifacts, contract, route, preflight, persistedPreflight, ledger };
|
|
200
|
+
}
|