@cassiomc1/forgeloop 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/DOCS_INDEX.md +60 -0
  2. package/EXECUTION_STATE.md +9 -0
  3. package/LOOP_ENGINEERING.md +15 -0
  4. package/LOOP_SYSTEM_DESIGN.md +8 -0
  5. package/PROTOCOL_INTEGRATION.md +8 -0
  6. package/QUALITY_SCORECARD.md +2 -0
  7. package/README.md +183 -654
  8. package/TERMINOLOGY.md +4 -0
  9. package/THREAT_MODEL.md +9 -0
  10. package/docs/assets/forgeloop-flow.svg +1 -0
  11. package/docs/forgeloop-flow.mmd +51 -0
  12. package/package.json +16 -3
  13. package/schemas/continuity.schema.json +57 -0
  14. package/scripts/CI_VALIDATORS.md +32 -0
  15. package/src/cli.js +307 -204
  16. package/src/commands/clear-continuity.js +9 -0
  17. package/src/commands/continuity.js +25 -0
  18. package/src/commands/doctor.js +17 -2
  19. package/src/commands/reconcile-continuity.js +23 -0
  20. package/src/commands/record-continuity.js +63 -0
  21. package/src/commands/status.js +14 -1
  22. package/src/commands/update.js +12 -13
  23. package/src/commands/validate-protocol.js +19 -0
  24. package/src/core/artifacts.js +1 -0
  25. package/src/core/bundles.js +6 -0
  26. package/src/core/command-resolution.js +295 -0
  27. package/src/core/command-tokenizer.js +122 -0
  28. package/src/core/conformance.js +8 -2
  29. package/src/core/continuity-cli-options.js +58 -0
  30. package/src/core/continuity-conformance.js +46 -0
  31. package/src/core/continuity-observability.js +20 -0
  32. package/src/core/continuity-reconciliation.js +224 -0
  33. package/src/core/continuity.js +245 -0
  34. package/src/core/inspect.js +9 -1
  35. package/src/core/installation-authority.js +178 -0
  36. package/src/core/json-safety.js +17 -13
  37. package/src/core/next-action-artifacts.js +118 -0
  38. package/src/core/next-action-continuity.js +65 -0
  39. package/src/core/next-action-model.js +127 -0
  40. package/src/core/next-action-phases.js +12 -0
  41. package/src/core/next-action.js +22 -249
  42. package/src/core/npm-classifier.js +343 -0
  43. package/src/core/package-manager-classifiers.js +37 -0
  44. package/src/core/preflight-consistency.js +221 -0
  45. package/src/core/preflight-loaders.js +112 -0
  46. package/src/core/preflight-model.js +83 -0
  47. package/src/core/preflight.js +34 -444
  48. package/src/core/protocol.js +7 -0
  49. package/src/core/schema-validation.js +15 -1
  50. package/src/core/templates.js +2 -0
  51. package/src/core/verification-capability.js +31 -1106
  52. package/src/core/verification-constants.js +61 -0
@@ -0,0 +1,343 @@
1
+ import {
2
+ NPM_CI_COMMANDS,
3
+ NPM_EXEC_COMMANDS,
4
+ NPM_INIT_COMMANDS,
5
+ NPM_INSTALL_COMMANDS,
6
+ NPM_INSTALL_CI_TEST_COMMANDS,
7
+ NPM_INSTALL_TEST_COMMANDS,
8
+ NPM_KNOWN_BOOLEAN_OPTIONS,
9
+ NPM_KNOWN_NON_INSTALLING_COMMANDS,
10
+ NPM_OPTIONS_WITH_VALUE,
11
+ NPM_SCRIPT_COMMANDS,
12
+ } from "./verification-constants.js";
13
+ import {
14
+ extractNpmExecTool,
15
+ extractToolFromArgs,
16
+ normalizeExecutableName,
17
+ tokenizeCommand,
18
+ unwrapCommandArgv,
19
+ } from "./command-tokenizer.js";
20
+
21
+ export function parseNpmInvocationArgs(rest) {
22
+ if (!Array.isArray(rest)) {
23
+ return {
24
+ subcommand: null,
25
+ subcommandIndex: -1,
26
+ args: [],
27
+ leadingOptions: [],
28
+ workspace: null,
29
+ workspaces: false,
30
+ ambiguous: true,
31
+ };
32
+ }
33
+
34
+ const leadingOptions = [];
35
+ let workspace = null;
36
+ let workspaces = false;
37
+
38
+ let i = 0;
39
+ while (i < rest.length) {
40
+ const arg = rest[i];
41
+ if (arg === "--" || !arg.startsWith("-")) break;
42
+
43
+ if (arg === "--workspaces" || arg === "--ws") {
44
+ workspaces = true;
45
+ leadingOptions.push(arg);
46
+ i += 1;
47
+ continue;
48
+ }
49
+
50
+ if (arg.startsWith("--workspace=")) {
51
+ workspace = arg.slice("--workspace=".length) || null;
52
+ leadingOptions.push(arg);
53
+ i += 1;
54
+ continue;
55
+ }
56
+
57
+ if (arg.startsWith("-w=")) {
58
+ workspace = arg.slice(3) || null;
59
+ leadingOptions.push(arg);
60
+ i += 1;
61
+ continue;
62
+ }
63
+
64
+ if (arg === "--workspace" || arg === "-w") {
65
+ leadingOptions.push(arg);
66
+ const value = rest[i + 1] ?? null;
67
+ if (value !== null && !value.startsWith("-")) {
68
+ workspace = value;
69
+ leadingOptions.push(value);
70
+ i += 2;
71
+ } else {
72
+ workspace = value;
73
+ i += 1;
74
+ }
75
+ continue;
76
+ }
77
+
78
+ if (NPM_OPTIONS_WITH_VALUE.has(arg)) {
79
+ leadingOptions.push(arg);
80
+ const value = rest[i + 1] ?? null;
81
+ if (value !== null && !value.startsWith("-")) {
82
+ leadingOptions.push(value);
83
+ i += 2;
84
+ } else {
85
+ i += 1;
86
+ }
87
+ continue;
88
+ }
89
+
90
+ if (/^--[^=]+=/.test(arg) || NPM_KNOWN_BOOLEAN_OPTIONS.has(arg)) {
91
+ leadingOptions.push(arg);
92
+ i += 1;
93
+ continue;
94
+ }
95
+
96
+ if (/^--/.test(arg)) {
97
+ const next = rest[i + 1];
98
+ if (next && !next.startsWith("-")) {
99
+ return {
100
+ subcommand: null,
101
+ subcommandIndex: -1,
102
+ args: [],
103
+ leadingOptions,
104
+ workspace,
105
+ workspaces,
106
+ ambiguous: true,
107
+ reason: "NPM_OPTION_VALUE_AMBIGUOUS",
108
+ };
109
+ }
110
+ leadingOptions.push(arg);
111
+ i += 1;
112
+ continue;
113
+ }
114
+
115
+ leadingOptions.push(arg);
116
+ i += 1;
117
+ }
118
+
119
+ const subcommand = rest[i] ? rest[i].toLowerCase() : null;
120
+ const trailingArgs = subcommand ? rest.slice(i + 1) : [];
121
+
122
+ for (let j = 0; j < trailingArgs.length; j++) {
123
+ const arg = trailingArgs[j];
124
+ if (arg === "--") break;
125
+ if (arg === "--workspaces" || arg === "--ws") {
126
+ workspaces = true;
127
+ } else if (arg.startsWith("--workspace=")) {
128
+ workspace = arg.slice("--workspace=".length) || null;
129
+ } else if (arg.startsWith("-w=")) {
130
+ workspace = arg.slice(3) || null;
131
+ } else if (arg === "--workspace" || arg === "-w") {
132
+ const value = trailingArgs[j + 1];
133
+ if (value && !value.startsWith("-")) {
134
+ workspace = value;
135
+ j += 1;
136
+ }
137
+ }
138
+ }
139
+
140
+ const ambiguous = subcommand === null;
141
+ return {
142
+ subcommand,
143
+ subcommandIndex: subcommand ? i : -1,
144
+ args: trailingArgs,
145
+ leadingOptions,
146
+ workspace,
147
+ workspaces,
148
+ ambiguous,
149
+ ...(ambiguous ? { reason: "NPM_SUBCOMMAND_AMBIGUOUS" } : {}),
150
+ };
151
+ }
152
+
153
+ export function parseNpmInvocation(argv) {
154
+ const tokens = unwrapCommandArgv(Array.isArray(argv) ? argv : tokenizeCommand(argv));
155
+ if (!tokens || tokens.length === 0) {
156
+ return {
157
+ subcommand: null,
158
+ subcommandIndex: -1,
159
+ args: [],
160
+ leadingOptions: [],
161
+ workspace: null,
162
+ workspaces: false,
163
+ ambiguous: true,
164
+ };
165
+ }
166
+ if (normalizeExecutableName(tokens[0]) !== "npm") {
167
+ return {
168
+ subcommand: null,
169
+ subcommandIndex: -1,
170
+ args: [],
171
+ leadingOptions: [],
172
+ workspace: null,
173
+ workspaces: false,
174
+ ambiguous: true,
175
+ };
176
+ }
177
+ return parseNpmInvocationArgs(tokens.slice(1));
178
+ }
179
+
180
+ export function npmWorkspaceSelection(npmInvocation) {
181
+ return {
182
+ scoped: Boolean(npmInvocation?.workspace || npmInvocation?.workspaces),
183
+ workspace: npmInvocation?.workspace ?? null,
184
+ allWorkspaces: npmInvocation?.workspaces === true,
185
+ };
186
+ }
187
+
188
+ export function classifyNpmInvocation(npm) {
189
+ const sub = npm.subcommand;
190
+
191
+ if (npm.ambiguous) {
192
+ return {
193
+ resolutionMode: "UNKNOWN",
194
+ mayInstall: true,
195
+ installer: "npm",
196
+ tool: null,
197
+ reason: npm.reason || "NPM_SUBCOMMAND_AMBIGUOUS",
198
+ };
199
+ }
200
+
201
+ if (NPM_EXEC_COMMANDS.has(sub)) {
202
+ return {
203
+ resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
204
+ mayInstall: true,
205
+ installer: `npm ${sub}`,
206
+ tool: extractNpmExecTool(npm.args),
207
+ };
208
+ }
209
+
210
+ if (NPM_INSTALL_COMMANDS.has(sub) || NPM_CI_COMMANDS.has(sub) || NPM_INSTALL_TEST_COMMANDS.has(sub) || NPM_INSTALL_CI_TEST_COMMANDS.has(sub)) {
211
+ let tool = null;
212
+ let installer = `npm ${sub}`;
213
+ if (NPM_INSTALL_COMMANDS.has(sub)) {
214
+ tool = extractToolFromArgs(npm.args);
215
+ installer = "npm";
216
+ }
217
+ return {
218
+ resolutionMode: "EXPLICIT_INSTALLATION",
219
+ mayInstall: true,
220
+ installer,
221
+ tool,
222
+ };
223
+ }
224
+
225
+ if (NPM_INIT_COMMANDS.has(sub)) {
226
+ let firstMeaningful = null;
227
+ for (let i = 0; i < npm.args.length; i++) {
228
+ const arg = npm.args[i];
229
+ if (arg === "--") {
230
+ const next = npm.args[i + 1];
231
+ firstMeaningful = next && !next.startsWith("-") ? next : null;
232
+ break;
233
+ }
234
+ if (!arg.startsWith("-")) {
235
+ firstMeaningful = arg;
236
+ break;
237
+ }
238
+ }
239
+ if (firstMeaningful) {
240
+ return {
241
+ resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
242
+ mayInstall: true,
243
+ installer: `npm ${sub}`,
244
+ tool: firstMeaningful,
245
+ };
246
+ }
247
+ return {
248
+ resolutionMode: "LOCAL_PACKAGE_BINARY",
249
+ mayInstall: false,
250
+ installer: null,
251
+ tool: null,
252
+ };
253
+ }
254
+
255
+ if (sub === "update" || sub === "up" || sub === "upgrade") {
256
+ return {
257
+ resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
258
+ mayInstall: true,
259
+ installer: `npm ${sub}`,
260
+ tool: extractToolFromArgs(npm.args),
261
+ };
262
+ }
263
+
264
+ if (sub === "audit") {
265
+ if (npm.args.includes("fix")) {
266
+ return {
267
+ resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
268
+ mayInstall: true,
269
+ installer: "npm audit fix",
270
+ tool: null,
271
+ };
272
+ }
273
+ return {
274
+ resolutionMode: "LOCAL_PACKAGE_BINARY",
275
+ mayInstall: false,
276
+ installer: null,
277
+ tool: null,
278
+ };
279
+ }
280
+
281
+ if (NPM_SCRIPT_COMMANDS.has(sub)) {
282
+ return {
283
+ resolutionMode: "LOCAL_PACKAGE_BINARY",
284
+ mayInstall: false,
285
+ installer: null,
286
+ tool: null,
287
+ dispatch: { kind: "npm-script-command" },
288
+ };
289
+ }
290
+
291
+ if (NPM_KNOWN_NON_INSTALLING_COMMANDS.has(sub)) {
292
+ return {
293
+ resolutionMode: "LOCAL_PACKAGE_BINARY",
294
+ mayInstall: false,
295
+ installer: null,
296
+ tool: null,
297
+ };
298
+ }
299
+
300
+ return {
301
+ resolutionMode: "UNKNOWN",
302
+ mayInstall: true,
303
+ installer: "npm",
304
+ tool: null,
305
+ reason: "NPM_COMMAND_UNCLASSIFIED",
306
+ };
307
+ }
308
+
309
+ export function getNpmScriptName(argv) {
310
+ const npm = parseNpmInvocation(argv);
311
+ if (npm.ambiguous || !npm.subcommand) return null;
312
+
313
+ const sub = npm.subcommand;
314
+ const args = npm.args;
315
+ if (["test", "t", "tst"].includes(sub)) return "test";
316
+ if (["start", "stop", "restart"].includes(sub)) return sub;
317
+ if (["run", "run-script", "rum", "urn"].includes(sub)) {
318
+ let afterDoubleDash = false;
319
+ for (let idx = 0; idx < args.length; idx++) {
320
+ const arg = args[idx];
321
+ if (arg === "--") {
322
+ afterDoubleDash = true;
323
+ if (args[idx + 1] && !args[idx + 1].startsWith("-")) return args[idx + 1];
324
+ continue;
325
+ }
326
+ if (!afterDoubleDash && !arg.startsWith("-")) {
327
+ if (idx > 0 && (args[idx - 1] === "-w" || args[idx - 1] === "--workspace" || NPM_OPTIONS_WITH_VALUE.has(args[idx - 1]))) continue;
328
+ return arg;
329
+ }
330
+ }
331
+ }
332
+ return null;
333
+ }
334
+
335
+ export function getNpmLifecycleCandidates({ scriptName, scripts = {} } = {}) {
336
+ if (scriptName === "restart") {
337
+ if (typeof scripts?.restart === "string" && scripts.restart.trim() !== "") {
338
+ return ["prerestart", "restart", "postrestart"];
339
+ }
340
+ return ["prerestart", "prestop", "stop", "poststop", "prestart", "start", "poststart", "postrestart"];
341
+ }
342
+ return [`pre${scriptName}`, scriptName, `post${scriptName}`];
343
+ }
@@ -0,0 +1,37 @@
1
+ import { extractToolFromArgs } from "./command-tokenizer.js";
2
+
3
+ const INSTALL_COMMANDS = new Set(["add", "install", "i"]);
4
+
5
+ function classifyPackageManagerInvocation(binary, args) {
6
+ const rest = Array.isArray(args) ? args : [];
7
+ if (rest[0] === "dlx") {
8
+ return {
9
+ resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
10
+ mayInstall: true,
11
+ installer: `${binary} dlx`,
12
+ tool: extractToolFromArgs(rest.slice(1)),
13
+ };
14
+ }
15
+ if (INSTALL_COMMANDS.has(rest[0])) {
16
+ return {
17
+ resolutionMode: "EXPLICIT_INSTALLATION",
18
+ mayInstall: true,
19
+ installer: binary,
20
+ tool: extractToolFromArgs(rest.slice(1)),
21
+ };
22
+ }
23
+ return {
24
+ resolutionMode: "LOCAL_PACKAGE_BINARY",
25
+ mayInstall: false,
26
+ installer: null,
27
+ tool: null,
28
+ };
29
+ }
30
+
31
+ export function classifyPnpmInvocation(args) {
32
+ return classifyPackageManagerInvocation("pnpm", args);
33
+ }
34
+
35
+ export function classifyYarnInvocation(args) {
36
+ return classifyPackageManagerInvocation("yarn", args);
37
+ }
@@ -0,0 +1,221 @@
1
+ import { ARTIFACT_PATHS } from "./artifacts.js";
2
+ import { readContract } from "./contract.js";
3
+ import { readPersistedRoute } from "./route-artifact.js";
4
+ import { appendProtocolEvent, validateEventLedger } from "./events.js";
5
+ import { readWorkState } from "./work-state.js";
6
+ import { assertStateIdentity } from "./completion-relationships.js";
7
+ import { ensureResumableState, synchronizePreflightState } from "./resumability.js";
8
+ import {
9
+ issue,
10
+ PREFLIGHT_IDENTITY_BARRIER_CODES,
11
+ preflightError,
12
+ sameStringSet,
13
+ sortIssues,
14
+ validatePersistedPreflight,
15
+ } from "./preflight-model.js";
16
+
17
+ async function readOptionalIdentityArtifact(readArtifact, invalidCode, artifactPath) {
18
+ try {
19
+ return await readArtifact();
20
+ } catch (error) {
21
+ if (error.code === "ARTIFACT_MISSING") return null;
22
+ throw preflightError(invalidCode, error.message, [artifactPath]);
23
+ }
24
+ }
25
+
26
+ export async function assertPreflightPersistenceSafety(target, packageRoot, taskId) {
27
+ let state;
28
+ try {
29
+ state = await readWorkState(target, packageRoot);
30
+ } catch (error) {
31
+ throw preflightError("E_STATE_INVALID", error.message, [ARTIFACT_PATHS.state]);
32
+ }
33
+ const contract = await readOptionalIdentityArtifact(
34
+ () => readContract(target, packageRoot),
35
+ "E_CONTRACT_INVALID",
36
+ ARTIFACT_PATHS.contract,
37
+ );
38
+ const route = await readOptionalIdentityArtifact(
39
+ () => readPersistedRoute(target, packageRoot),
40
+ "E_ROUTE_INVALID",
41
+ ARTIFACT_PATHS.route,
42
+ );
43
+ if (state && (contract || route)) assertStateIdentity({ contract, route, state });
44
+
45
+ if (taskId === "unknown") return null;
46
+ const ledger = await validateEventLedger(target, packageRoot);
47
+ if (!ledger.valid) {
48
+ const first = ledger.errors[0];
49
+ throw preflightError(first.code, first.message, [ARTIFACT_PATHS.events]);
50
+ }
51
+ if (ledger.events.some((event) => event.taskId !== taskId)) {
52
+ throw preflightError(
53
+ "E_PHASE_CHRONOLOGY_INVALID",
54
+ "Preflight cannot append events to a ledger owned by a different task",
55
+ [ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract],
56
+ );
57
+ }
58
+ return ledger;
59
+ }
60
+
61
+ export function assertPreflightResultPersistenceSafety(result) {
62
+ const identityError = result.errors.find((error) => PREFLIGHT_IDENTITY_BARRIER_CODES.has(error.code));
63
+ if (identityError) throw preflightError(identityError.code, identityError.message, identityError.artifacts);
64
+ }
65
+
66
+ function sameReadyPreflightEvent(event, result) {
67
+ return event.fingerprint === result.fingerprints.contract
68
+ && event.details?.routingFingerprint === result.fingerprints.routing
69
+ && sameStringSet(event.details?.requiredGates, result.requiredGates)
70
+ && sameStringSet(event.details?.satisfiedGates, result.satisfiedGates);
71
+ }
72
+
73
+ export async function validateReadyProtocolConsistency({
74
+ target,
75
+ packageRoot,
76
+ persisted,
77
+ current = null,
78
+ evaluateCurrentPreflight,
79
+ } = {}) {
80
+ if (persisted?.status !== "READY") return [];
81
+ const result = current ?? await evaluateCurrentPreflight({ target, packageRoot });
82
+ const errors = [...validatePersistedPreflight(persisted, result)];
83
+
84
+ let state = null;
85
+ try {
86
+ state = await readWorkState(target, packageRoot);
87
+ } catch (error) {
88
+ errors.push(issue("E_STATE_INVALID", error.message, [ARTIFACT_PATHS.state]));
89
+ }
90
+ if (!state) {
91
+ errors.push(issue(
92
+ "E_STATE_MISSING_AFTER_PREFLIGHT_READY",
93
+ "A persisted READY preflight must have a resumable work-state checkpoint",
94
+ [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.state],
95
+ ));
96
+ } else {
97
+ if (state.taskId !== persisted.taskId) {
98
+ errors.push(issue("E_STATE_TASK_MISMATCH", "The resumable checkpoint does not belong to the READY preflight task", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.preflight]));
99
+ }
100
+ if (state.contractFingerprint !== result.fingerprints.contract) {
101
+ errors.push(issue("E_CONTRACT_STALE", "The resumable checkpoint does not match the READY contract fingerprint", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract]));
102
+ }
103
+ if (state.routeFingerprint !== result.fingerprints.routing) {
104
+ errors.push(issue("E_ROUTE_STALE", "The resumable checkpoint does not match the READY routing fingerprint", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.route]));
105
+ }
106
+ if (JSON.stringify(state.selectedGuides) !== JSON.stringify(result.routing.guides)) {
107
+ errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "The resumable checkpoint guides do not match the READY routing result", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.route]));
108
+ }
109
+ if (!sameStringSet(state.requiredGates ?? [], persisted.requiredGates)
110
+ || !sameStringSet(state.satisfiedGates ?? [], persisted.satisfiedGates)) {
111
+ errors.push(issue("E_PREFLIGHT_GATES_STALE", "The resumable checkpoint gate sets do not match the READY preflight", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.gates]));
112
+ }
113
+ }
114
+
115
+ const ledger = await validateEventLedger(target, packageRoot);
116
+ if (!ledger.valid) {
117
+ errors.push(...ledger.errors.map((error) => issue(error.code ?? "E_EVENT_INVALID", error.message, [ARTIFACT_PATHS.events])));
118
+ }
119
+ const events = ledger.events ?? [];
120
+ for (const requiredEvent of ["CONTRACT_VALIDATED", "ROUTE_VALIDATED"]) {
121
+ if (!events.some((event) => event.event === requiredEvent && event.taskId === persisted.taskId)) {
122
+ errors.push(issue("E_PREFLIGHT_EVENT_MISSING", `READY preflight is missing lifecycle event: ${requiredEvent}`, [ARTIFACT_PATHS.events, ARTIFACT_PATHS.preflight]));
123
+ }
124
+ }
125
+ for (const gate of persisted.satisfiedGates ?? []) {
126
+ if (!events.some((event) => event.event === "GATE_SATISFIED"
127
+ && event.taskId === persisted.taskId
128
+ && event.details?.gate === gate)) {
129
+ errors.push(issue("E_PREFLIGHT_GATE_EVENT_MISSING", `READY preflight is missing lifecycle gate event: ${gate}`, [ARTIFACT_PATHS.events, `${ARTIFACT_PATHS.gates}/${gate}.json`]));
130
+ }
131
+ }
132
+ const readyEvents = events.filter((event) => event.event === "PREFLIGHT_READY" && event.taskId === persisted.taskId);
133
+ if (readyEvents.length === 0) {
134
+ errors.push(issue("E_PREFLIGHT_READY_EVENT_MISSING", "Persisted READY preflight is missing the matching PREFLIGHT_READY lifecycle event", [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events]));
135
+ } else if (!readyEvents.some((event) => sameReadyPreflightEvent(event, result))) {
136
+ errors.push(issue("E_PREFLIGHT_READY_EVENT_MISMATCH", "PREFLIGHT_READY lifecycle details do not match the persisted READY preflight", [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events]));
137
+ }
138
+ return sortIssues(errors);
139
+ }
140
+
141
+ function sameBlockedPreflightEvent(event, result) {
142
+ return event.event === "PREFLIGHT_BLOCKED"
143
+ && event.taskId === result.taskId
144
+ && event.fingerprint === (result.fingerprints.contract ?? undefined)
145
+ && sameStringSet(event.details?.requiredGates, result.requiredGates)
146
+ && sameStringSet(event.details?.satisfiedGates, result.satisfiedGates)
147
+ && event.details?.routingFingerprint === result.fingerprints.routing;
148
+ }
149
+
150
+ export function assertExistingReadyLifecycleCompatibility(ledger, result) {
151
+ if (result.status !== "READY") return;
152
+ const existingReady = ledger?.events?.find((event) => event.event === "PREFLIGHT_READY" && event.taskId === result.taskId);
153
+ if (existingReady && !sameReadyPreflightEvent(existingReady, result)) {
154
+ throw preflightError(
155
+ "E_PHASE_CHRONOLOGY_INVALID",
156
+ "PREFLIGHT_READY already exists with different READY preflight details; repair the contract, route, or gate lifecycle before refreshing preflight",
157
+ [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route, ARTIFACT_PATHS.gates],
158
+ );
159
+ }
160
+ }
161
+
162
+ export async function appendActivationEvents(target, packageRoot, ledger, result) {
163
+ const events = [...(ledger?.events ?? [])];
164
+ const hasEvent = (eventName) => events.some((event) => event.event === eventName && event.taskId === result.taskId);
165
+ const append = async (input) => {
166
+ const event = await appendProtocolEvent(target, input, packageRoot);
167
+ events.push(event);
168
+ };
169
+
170
+ if (events.length === 0) await append({ taskId: result.taskId, event: "TASK_RECEIVED" });
171
+ if (result.contract.status === "valid" && !hasEvent("CONTRACT_VALIDATED")) {
172
+ await append({ taskId: result.taskId, event: "CONTRACT_VALIDATED", fingerprint: result.fingerprints.contract });
173
+ }
174
+ if (result.routing.status === "valid" && !hasEvent("ROUTE_VALIDATED")) {
175
+ await append({ taskId: result.taskId, event: "ROUTE_VALIDATED", fingerprint: result.fingerprints.routing });
176
+ }
177
+ for (const gate of result.satisfiedGates) {
178
+ if (!events.some((event) => event.event === "GATE_SATISFIED"
179
+ && event.taskId === result.taskId
180
+ && event.details?.gate === gate)) {
181
+ await append({ taskId: result.taskId, event: "GATE_SATISFIED", details: { gate } });
182
+ }
183
+ }
184
+
185
+ const existingReady = events.find((event) => event.event === "PREFLIGHT_READY" && event.taskId === result.taskId);
186
+ if (result.status === "READY") {
187
+ if (existingReady && !sameReadyPreflightEvent(existingReady, result)) {
188
+ throw preflightError(
189
+ "E_PHASE_CHRONOLOGY_INVALID",
190
+ "PREFLIGHT_READY already exists with different READY preflight details; repair the contract, route, or gate lifecycle before refreshing preflight",
191
+ [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route, ARTIFACT_PATHS.gates],
192
+ );
193
+ }
194
+ if (!existingReady) {
195
+ await append({
196
+ taskId: result.taskId,
197
+ event: "PREFLIGHT_READY",
198
+ fingerprint: result.fingerprints.contract ?? undefined,
199
+ details: {
200
+ requiredGates: result.requiredGates,
201
+ satisfiedGates: result.satisfiedGates,
202
+ routingFingerprint: result.fingerprints.routing,
203
+ },
204
+ });
205
+ }
206
+ } else if (!events.some((event) => sameBlockedPreflightEvent(event, result))) {
207
+ await append({
208
+ taskId: result.taskId,
209
+ event: "PREFLIGHT_BLOCKED",
210
+ fingerprint: result.fingerprints.contract ?? undefined,
211
+ details: {
212
+ requiredGates: result.requiredGates,
213
+ satisfiedGates: result.satisfiedGates,
214
+ routingFingerprint: result.fingerprints.routing,
215
+ },
216
+ });
217
+ }
218
+ return events;
219
+ }
220
+
221
+ export { ensureResumableState, synchronizePreflightState };