@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/phase.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { ARTIFACT_PATHS } from "./artifacts.js";
|
|
2
|
-
import { readJsonArtifact } from "./artifacts.js";
|
|
1
|
+
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
3
2
|
import { readContract } from "./contract.js";
|
|
4
|
-
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
3
|
+
import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger } from "./events.js";
|
|
5
4
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
6
5
|
import { assertWorkPhase, isValidTransition } from "./protocol.js";
|
|
7
6
|
import { readWorkState, writeWorkState } from "./work-state.js";
|
|
8
7
|
import { evaluateCompletion } from "./completion.js";
|
|
8
|
+
import { evaluatePreflight, validatePersistedPreflight } from "./preflight.js";
|
|
9
|
+
import { requiredEvidenceForTarget } from "./completion-artifacts.js";
|
|
10
|
+
import { assertCompletionRelationships, assertStateIdentity } from "./completion-relationships.js";
|
|
11
|
+
import { createReceipt, validateReceipt } from "./receipt.js";
|
|
12
|
+
import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
|
|
9
13
|
|
|
10
14
|
function phaseError(code, message, artifacts = []) {
|
|
11
15
|
const error = new Error(message);
|
|
@@ -24,6 +28,72 @@ const PHASE_EVENTS = Object.freeze({
|
|
|
24
28
|
COMPLETE: "COMPLETION_VALIDATED",
|
|
25
29
|
});
|
|
26
30
|
|
|
31
|
+
const LATE_PHASES = new Set([
|
|
32
|
+
"EXECUTING",
|
|
33
|
+
"VERIFYING",
|
|
34
|
+
"DIAGNOSING",
|
|
35
|
+
"CORRECTING",
|
|
36
|
+
"REVIEWING",
|
|
37
|
+
"COMPLETE",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
async function assertPersistedStateIdentity(target, state, toPhase, packageRoot) {
|
|
41
|
+
const requireContract = LATE_PHASES.has(state.phase)
|
|
42
|
+
|| ["CONTRACT_READY", "ROUTED", "EXECUTING"].includes(toPhase);
|
|
43
|
+
const requireRoute = LATE_PHASES.has(state.phase)
|
|
44
|
+
|| ["ROUTED", "EXECUTING"].includes(toPhase);
|
|
45
|
+
let contract = null;
|
|
46
|
+
let route = null;
|
|
47
|
+
try {
|
|
48
|
+
contract = await readContract(target, packageRoot);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.code === "ARTIFACT_MISSING" && !requireContract) {
|
|
51
|
+
contract = null;
|
|
52
|
+
} else {
|
|
53
|
+
throw phaseError(
|
|
54
|
+
requireContract ? "E_PHASE_PREREQUISITE_MISSING" : error.code ?? "E_CONTRACT_INVALID",
|
|
55
|
+
`${requireContract ? `Phase ${toPhase} requires current contract` : "Unable to validate current contract"}: ${error.message}`,
|
|
56
|
+
[ARTIFACT_PATHS.contract],
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
route = await readPersistedRoute(target, packageRoot);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error.code === "ARTIFACT_MISSING" && !requireRoute) {
|
|
64
|
+
route = null;
|
|
65
|
+
} else {
|
|
66
|
+
throw phaseError(
|
|
67
|
+
requireRoute ? "E_PHASE_PREREQUISITE_MISSING" : error.code ?? "E_ROUTE_INVALID",
|
|
68
|
+
`${requireRoute ? `Phase ${toPhase} requires persisted route` : "Unable to validate persisted route"}: ${error.message}`,
|
|
69
|
+
[ARTIFACT_PATHS.route],
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!contract && !route) return;
|
|
74
|
+
try {
|
|
75
|
+
assertStateIdentity({ contract, route, state });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw phaseError(
|
|
78
|
+
error.code,
|
|
79
|
+
error.message,
|
|
80
|
+
error.artifacts,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function reconcileImplementationStep(state, toPhase) {
|
|
86
|
+
if (state.phase !== "EXECUTING" || toPhase !== "VERIFYING") return state;
|
|
87
|
+
if (!state.pendingSteps.includes("implementation")) return state;
|
|
88
|
+
return {
|
|
89
|
+
...state,
|
|
90
|
+
completedSteps: state.completedSteps.includes("implementation")
|
|
91
|
+
? [...state.completedSteps]
|
|
92
|
+
: [...state.completedSteps, "implementation"],
|
|
93
|
+
pendingSteps: state.pendingSteps.filter((step) => step !== "implementation"),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
27
97
|
async function assertPhasePrerequisites(target, state, toPhase, packageRoot) {
|
|
28
98
|
if (toPhase === "CONTRACT_READY" || toPhase === "ROUTED" || toPhase === "EXECUTING") {
|
|
29
99
|
try {
|
|
@@ -39,21 +109,11 @@ async function assertPhasePrerequisites(target, state, toPhase, packageRoot) {
|
|
|
39
109
|
throw phaseError("E_PHASE_PREREQUISITE_MISSING", `Phase ${toPhase} requires ${ARTIFACT_PATHS.route}: ${error.message}`, [ARTIFACT_PATHS.route]);
|
|
40
110
|
}
|
|
41
111
|
}
|
|
42
|
-
if (toPhase
|
|
43
|
-
let preflight;
|
|
112
|
+
if (hasExecutionStarted(toPhase)) {
|
|
44
113
|
try {
|
|
45
|
-
|
|
114
|
+
await assertExecutionPrerequisites({ target, state, packageRoot });
|
|
46
115
|
} catch (error) {
|
|
47
|
-
throw phaseError(
|
|
48
|
-
}
|
|
49
|
-
if (preflight.value.status !== "READY") {
|
|
50
|
-
throw phaseError("E_PREFLIGHT_NOT_READY", "EXECUTING requires a READY preflight", [ARTIFACT_PATHS.preflight]);
|
|
51
|
-
}
|
|
52
|
-
const ledger = await validateEventLedger(target, packageRoot);
|
|
53
|
-
for (const requiredEvent of ["CONTRACT_VALIDATED", "ROUTE_VALIDATED", "PREFLIGHT_READY"]) {
|
|
54
|
-
if (!ledger.events.some((event) => event.event === requiredEvent)) {
|
|
55
|
-
throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `EXECUTING requires a ${requiredEvent} protocol event`, [ARTIFACT_PATHS.events]);
|
|
56
|
-
}
|
|
116
|
+
throw phaseError(error.code, error.message, error.artifacts);
|
|
57
117
|
}
|
|
58
118
|
}
|
|
59
119
|
if (toPhase === "COMPLETE" && state.verificationEvidence.length === 0) {
|
|
@@ -72,17 +132,81 @@ export async function advanceWorkState(target, toPhase, { packageRoot, now = new
|
|
|
72
132
|
const state = await readWorkState(target, packageRoot);
|
|
73
133
|
if (!state) throw phaseError("E_PHASE_PREREQUISITE_MISSING", "Cannot advance without work state", [ARTIFACT_PATHS.state]);
|
|
74
134
|
await assertPhasePrerequisites(target, state, toPhase, packageRoot);
|
|
135
|
+
await assertPersistedStateIdentity(target, state, toPhase, packageRoot);
|
|
75
136
|
if (!isValidTransition(state.phase, toPhase)) {
|
|
76
137
|
throw phaseError("E_PHASE_TRANSITION_INVALID", `Invalid work-state transition: ${state.phase} -> ${toPhase}`);
|
|
77
138
|
}
|
|
139
|
+
const ledger = await validateEventLedger(target, packageRoot);
|
|
140
|
+
if (!ledger.valid) {
|
|
141
|
+
const first = ledger.errors[0];
|
|
142
|
+
throw phaseError(first.code, first.message, [ARTIFACT_PATHS.events]);
|
|
143
|
+
}
|
|
144
|
+
if (ledger.events.some((event) => event.taskId !== state.taskId)) {
|
|
145
|
+
throw phaseError(
|
|
146
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
147
|
+
"Cannot advance work state with lifecycle events from a different task",
|
|
148
|
+
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.state],
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const eventType = PHASE_EVENTS[toPhase];
|
|
152
|
+
if (eventType && ledger.events.some((event) => event.taskId === state.taskId && event.event === eventType)) {
|
|
153
|
+
throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `Lifecycle milestone already exists: ${eventType}`, [ARTIFACT_PATHS.events]);
|
|
154
|
+
}
|
|
155
|
+
const milestoneIndex = LIFECYCLE_MILESTONES.indexOf(eventType);
|
|
156
|
+
if (milestoneIndex >= 0) {
|
|
157
|
+
const lastMilestone = ledger.events.reduce((last, event) => Math.max(last, LIFECYCLE_MILESTONES.indexOf(event.event)), -1);
|
|
158
|
+
if (lastMilestone !== milestoneIndex - 1) {
|
|
159
|
+
throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `Phase ${toPhase} cannot append ${eventType} after the current lifecycle ledger`, [ARTIFACT_PATHS.events]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const reconciled = reconcileImplementationStep(state, toPhase);
|
|
78
163
|
const next = {
|
|
79
|
-
...
|
|
164
|
+
...reconciled,
|
|
80
165
|
previousPhase: state.phase,
|
|
81
166
|
phase: toPhase,
|
|
82
167
|
lastUpdated: now,
|
|
83
168
|
};
|
|
169
|
+
let nextReceipt = null;
|
|
170
|
+
try {
|
|
171
|
+
const receipt = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
|
|
172
|
+
const contract = await readContract(target, packageRoot);
|
|
173
|
+
const route = await readPersistedRoute(target, packageRoot);
|
|
174
|
+
const preflight = await evaluatePreflight({ target, packageRoot });
|
|
175
|
+
const requiredEvidence = await requiredEvidenceForTarget({
|
|
176
|
+
target,
|
|
177
|
+
contract,
|
|
178
|
+
route,
|
|
179
|
+
packageRoot,
|
|
180
|
+
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
181
|
+
});
|
|
182
|
+
await validateReceipt(receipt.value, packageRoot);
|
|
183
|
+
assertCompletionRelationships({
|
|
184
|
+
contract,
|
|
185
|
+
route,
|
|
186
|
+
state,
|
|
187
|
+
receipt: receipt.value,
|
|
188
|
+
requiredEvidence,
|
|
189
|
+
requireRequiredChecks: false,
|
|
190
|
+
});
|
|
191
|
+
nextReceipt = await createReceipt({
|
|
192
|
+
...receipt.value,
|
|
193
|
+
stateFingerprint: canonicalFingerprint(next),
|
|
194
|
+
}, packageRoot);
|
|
195
|
+
assertCompletionRelationships({
|
|
196
|
+
contract,
|
|
197
|
+
route,
|
|
198
|
+
state: next,
|
|
199
|
+
receipt: nextReceipt,
|
|
200
|
+
requiredEvidence,
|
|
201
|
+
requireRequiredChecks: false,
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
205
|
+
}
|
|
84
206
|
await writeWorkState(target, next, { packageRoot });
|
|
85
|
-
|
|
207
|
+
if (nextReceipt) {
|
|
208
|
+
await writeJsonArtifact(target, ARTIFACT_PATHS.receipt, nextReceipt, "execution-receipt", packageRoot);
|
|
209
|
+
}
|
|
86
210
|
if (eventType) await appendProtocolEvent(target, { taskId: state.taskId, event: eventType, at: now }, packageRoot);
|
|
87
211
|
return next;
|
|
88
212
|
}
|
package/src/core/preflight.js
CHANGED
|
@@ -5,12 +5,13 @@ import { requiredGatesForGuides } from "./guide-metadata.js";
|
|
|
5
5
|
import { assertRouteInvariants } from "./router.js";
|
|
6
6
|
import { assertSourceProvenance } from "./sources.js";
|
|
7
7
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
8
|
-
import { appendProtocolEvent } from "./events.js";
|
|
8
|
+
import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger } from "./events.js";
|
|
9
9
|
import { readWorkState } from "./work-state.js";
|
|
10
10
|
import { readConfig } from "./config.js";
|
|
11
11
|
import { assertSafePath, ensureWithin, fileExists, readBytes } from "./filesystem.js";
|
|
12
12
|
import { sha256 } from "./manifest.js";
|
|
13
13
|
import { validateProfileSources } from "./profile.js";
|
|
14
|
+
import { assertStateIdentity } from "./completion-relationships.js";
|
|
14
15
|
|
|
15
16
|
const PREVIEW_DECISION_LIMIT = 10;
|
|
16
17
|
const PREVIEW_DECISION_MAX_LENGTH = 240;
|
|
@@ -29,6 +30,66 @@ function sortIssues(errors) {
|
|
|
29
30
|
|| left.message.localeCompare(right.message));
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function preflightError(code, message, artifacts = []) {
|
|
34
|
+
const error = new Error(message);
|
|
35
|
+
error.code = code;
|
|
36
|
+
error.artifacts = artifacts;
|
|
37
|
+
return error;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sameStringSet(left, right) {
|
|
41
|
+
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
42
|
+
const leftSet = new Set(left);
|
|
43
|
+
const rightSet = new Set(right);
|
|
44
|
+
return leftSet.size === left.length
|
|
45
|
+
&& rightSet.size === right.length
|
|
46
|
+
&& leftSet.size === rightSet.size
|
|
47
|
+
&& [...leftSet].every((value) => rightSet.has(value));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function validatePersistedPreflight(persisted, current) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
if (persisted?.status !== "READY") {
|
|
53
|
+
errors.push(issue("E_PREFLIGHT_NOT_READY", "A persisted READY preflight is required", [ARTIFACT_PATHS.preflight]));
|
|
54
|
+
return errors;
|
|
55
|
+
}
|
|
56
|
+
if (current?.status !== "READY") {
|
|
57
|
+
errors.push(issue("E_PREFLIGHT_NOT_READY", "The current preflight evaluation is not READY", [ARTIFACT_PATHS.preflight]));
|
|
58
|
+
}
|
|
59
|
+
if (persisted.taskId !== current?.taskId) {
|
|
60
|
+
errors.push(issue(
|
|
61
|
+
"E_PREFLIGHT_TASK_MISMATCH",
|
|
62
|
+
"Persisted preflight does not belong to the current task",
|
|
63
|
+
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.contract],
|
|
64
|
+
));
|
|
65
|
+
}
|
|
66
|
+
if (persisted.fingerprints?.contract !== current?.fingerprints?.contract
|
|
67
|
+
|| persisted.contract?.fingerprint !== current?.contract?.fingerprint) {
|
|
68
|
+
errors.push(issue(
|
|
69
|
+
"E_PREFLIGHT_CONTRACT_STALE",
|
|
70
|
+
"Persisted preflight does not match the current contract fingerprint",
|
|
71
|
+
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.contract],
|
|
72
|
+
));
|
|
73
|
+
}
|
|
74
|
+
if (persisted.fingerprints?.routing !== current?.fingerprints?.routing
|
|
75
|
+
|| persisted.routing?.fingerprint !== current?.routing?.fingerprint) {
|
|
76
|
+
errors.push(issue(
|
|
77
|
+
"E_PREFLIGHT_ROUTE_STALE",
|
|
78
|
+
"Persisted preflight does not match the current routing fingerprint",
|
|
79
|
+
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.route],
|
|
80
|
+
));
|
|
81
|
+
}
|
|
82
|
+
if (!sameStringSet(persisted.requiredGates, current?.requiredGates)
|
|
83
|
+
|| !sameStringSet(persisted.satisfiedGates, current?.satisfiedGates)) {
|
|
84
|
+
errors.push(issue(
|
|
85
|
+
"E_PREFLIGHT_GATES_STALE",
|
|
86
|
+
"Persisted preflight gate sets do not match the current evaluation",
|
|
87
|
+
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.gates],
|
|
88
|
+
));
|
|
89
|
+
}
|
|
90
|
+
return sortIssues(errors);
|
|
91
|
+
}
|
|
92
|
+
|
|
32
93
|
async function readProfile(target) {
|
|
33
94
|
const relativePath = "PROJECT_PROFILE.md";
|
|
34
95
|
await assertSafePath(target, relativePath);
|
|
@@ -219,11 +280,112 @@ export async function evaluatePreflight({ target, packageRoot, strict = false }
|
|
|
219
280
|
};
|
|
220
281
|
}
|
|
221
282
|
|
|
283
|
+
async function readOptionalIdentityArtifact(readArtifact, invalidCode, artifactPath) {
|
|
284
|
+
try {
|
|
285
|
+
return await readArtifact();
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (error.code === "ARTIFACT_MISSING") return null;
|
|
288
|
+
throw preflightError(invalidCode, error.message, [artifactPath]);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function assertPreflightPersistenceSafety(target, packageRoot, taskId) {
|
|
293
|
+
let state;
|
|
294
|
+
try {
|
|
295
|
+
state = await readWorkState(target, packageRoot);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw preflightError("E_STATE_INVALID", error.message, [ARTIFACT_PATHS.state]);
|
|
298
|
+
}
|
|
299
|
+
const contract = await readOptionalIdentityArtifact(
|
|
300
|
+
() => readContract(target, packageRoot),
|
|
301
|
+
"E_CONTRACT_INVALID",
|
|
302
|
+
ARTIFACT_PATHS.contract,
|
|
303
|
+
);
|
|
304
|
+
const route = await readOptionalIdentityArtifact(
|
|
305
|
+
() => readPersistedRoute(target, packageRoot),
|
|
306
|
+
"E_ROUTE_INVALID",
|
|
307
|
+
ARTIFACT_PATHS.route,
|
|
308
|
+
);
|
|
309
|
+
if (state) {
|
|
310
|
+
if (contract || route) assertStateIdentity({ contract, route, state });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (taskId === "unknown") return null;
|
|
314
|
+
const ledger = await validateEventLedger(target, packageRoot);
|
|
315
|
+
if (!ledger.valid) {
|
|
316
|
+
const first = ledger.errors[0];
|
|
317
|
+
throw preflightError(first.code, first.message, [ARTIFACT_PATHS.events]);
|
|
318
|
+
}
|
|
319
|
+
if (ledger.events.some((event) => event.taskId !== taskId)) {
|
|
320
|
+
throw preflightError(
|
|
321
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
322
|
+
"Preflight cannot append events to a ledger owned by a different task",
|
|
323
|
+
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract],
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return ledger;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const PREFLIGHT_IDENTITY_BARRIER_CODES = new Set([
|
|
330
|
+
"E_CONTRACT_STALE",
|
|
331
|
+
"E_GATE_TASK_MISMATCH",
|
|
332
|
+
"E_ROUTE_STALE",
|
|
333
|
+
"E_STATE_TASK_MISMATCH",
|
|
334
|
+
"E_ROUTE_GUIDE_MISMATCH",
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
function assertPreflightResultPersistenceSafety(result) {
|
|
338
|
+
const identityError = result.errors.find((error) => PREFLIGHT_IDENTITY_BARRIER_CODES.has(error.code));
|
|
339
|
+
if (identityError) {
|
|
340
|
+
throw preflightError(identityError.code, identityError.message, identityError.artifacts);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function sameReadyPreflightEvent(event, result) {
|
|
345
|
+
return event.fingerprint === result.fingerprints.contract
|
|
346
|
+
&& event.details?.routingFingerprint === result.fingerprints.routing
|
|
347
|
+
&& sameStringSet(event.details?.requiredGates, result.requiredGates)
|
|
348
|
+
&& sameStringSet(event.details?.satisfiedGates, result.satisfiedGates);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function planReadyPreflightLifecycleWrite(ledger, result) {
|
|
352
|
+
if (!ledger) return { appendEvents: false };
|
|
353
|
+
const existing = ledger.events.find((event) => event.event === "PREFLIGHT_READY");
|
|
354
|
+
if (existing) {
|
|
355
|
+
if (sameReadyPreflightEvent(existing, result)) return { appendEvents: false };
|
|
356
|
+
throw preflightError(
|
|
357
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
358
|
+
"PREFLIGHT_READY already exists with different READY preflight details; repair the contract, route, or gate lifecycle before refreshing preflight",
|
|
359
|
+
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route, ARTIFACT_PATHS.gates],
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const lastMilestone = ledger.events.reduce(
|
|
363
|
+
(last, event) => Math.max(last, LIFECYCLE_MILESTONES.indexOf(event.event)),
|
|
364
|
+
-1,
|
|
365
|
+
);
|
|
366
|
+
const routeMilestone = LIFECYCLE_MILESTONES.indexOf("ROUTE_VALIDATED");
|
|
367
|
+
const preflightMilestone = LIFECYCLE_MILESTONES.indexOf("PREFLIGHT_READY");
|
|
368
|
+
if (lastMilestone < routeMilestone) return { appendEvents: false };
|
|
369
|
+
if (lastMilestone !== preflightMilestone - 1) {
|
|
370
|
+
throw preflightError(
|
|
371
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
372
|
+
"PREFLIGHT_READY cannot be appended after the current lifecycle ledger",
|
|
373
|
+
[ARTIFACT_PATHS.events],
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return { appendEvents: true };
|
|
377
|
+
}
|
|
378
|
+
|
|
222
379
|
export async function runPreflight({ target, packageRoot, strict = false, persist = true } = {}) {
|
|
223
380
|
const result = await evaluatePreflight({ target, packageRoot, strict });
|
|
224
381
|
if (persist) {
|
|
382
|
+
const ledger = await assertPreflightPersistenceSafety(target, packageRoot, result.taskId);
|
|
383
|
+
assertPreflightResultPersistenceSafety(result);
|
|
384
|
+
const lifecycleWrite = result.status === "READY"
|
|
385
|
+
? planReadyPreflightLifecycleWrite(ledger, result)
|
|
386
|
+
: { appendEvents: true };
|
|
225
387
|
await writeJsonArtifact(target, ARTIFACT_PATHS.preflight, result, "preflight", packageRoot);
|
|
226
|
-
if (result.taskId !== "unknown") {
|
|
388
|
+
if (result.taskId !== "unknown" && lifecycleWrite.appendEvents) {
|
|
227
389
|
for (const gate of result.satisfiedGates) {
|
|
228
390
|
await appendProtocolEvent(target, {
|
|
229
391
|
taskId: result.taskId,
|
|
@@ -238,6 +400,7 @@ export async function runPreflight({ target, packageRoot, strict = false, persis
|
|
|
238
400
|
details: {
|
|
239
401
|
requiredGates: result.requiredGates,
|
|
240
402
|
satisfiedGates: result.satisfiedGates,
|
|
403
|
+
routingFingerprint: result.fingerprints.routing,
|
|
241
404
|
},
|
|
242
405
|
}, packageRoot);
|
|
243
406
|
}
|