@cassiomc1/forgeloop 0.1.14 → 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.
- package/.forgeloop/forgeloop.gitignore +1 -0
- package/DOCS_INDEX.md +60 -0
- package/EXECUTION_STATE.md +9 -0
- package/LOOP_ENGINEERING.md +44 -6
- package/LOOP_SYSTEM_DESIGN.md +8 -0
- package/PROTOCOL_INTEGRATION.md +41 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +183 -643
- package/TERMINOLOGY.md +4 -0
- package/THREAT_MODEL.md +14 -0
- package/docs/assets/forgeloop-flow.svg +1 -0
- package/docs/forgeloop-flow.mmd +51 -0
- package/package.json +16 -3
- package/schemas/check.schema.json +2 -0
- package/schemas/continuity.schema.json +57 -0
- package/schemas/execution.schema.json +63 -0
- package/scripts/CI_VALIDATORS.md +32 -0
- package/src/cli.js +351 -196
- package/src/commands/clear-continuity.js +9 -0
- package/src/commands/continuity.js +25 -0
- package/src/commands/doctor.js +17 -2
- package/src/commands/reconcile-continuity.js +23 -0
- package/src/commands/record-continuity.js +63 -0
- package/src/commands/run-check.js +83 -0
- package/src/commands/status.js +14 -1
- package/src/commands/update.js +12 -13
- package/src/commands/validate-protocol.js +37 -0
- package/src/core/artifacts.js +9 -0
- package/src/core/bundles.js +78 -0
- package/src/core/checks.js +16 -0
- package/src/core/command-resolution.js +295 -0
- package/src/core/command-tokenizer.js +122 -0
- package/src/core/completion-artifacts.js +229 -47
- package/src/core/completion.js +19 -1
- package/src/core/conformance.js +8 -2
- package/src/core/continuity-cli-options.js +58 -0
- package/src/core/continuity-conformance.js +46 -0
- package/src/core/continuity-observability.js +20 -0
- package/src/core/continuity-reconciliation.js +224 -0
- package/src/core/continuity.js +245 -0
- package/src/core/evidence-readiness.js +26 -1
- package/src/core/execution.js +185 -0
- package/src/core/inspect.js +9 -1
- package/src/core/installation-authority.js +178 -0
- package/src/core/json-safety.js +17 -13
- package/src/core/next-action-artifacts.js +118 -0
- package/src/core/next-action-continuity.js +65 -0
- package/src/core/next-action-model.js +127 -0
- package/src/core/next-action-phases.js +12 -0
- package/src/core/next-action.js +22 -249
- package/src/core/npm-classifier.js +343 -0
- package/src/core/package-manager-classifiers.js +37 -0
- package/src/core/preflight-consistency.js +221 -0
- package/src/core/preflight-loaders.js +112 -0
- package/src/core/preflight-model.js +83 -0
- package/src/core/preflight.js +34 -444
- package/src/core/protocol.js +7 -0
- package/src/core/schema-validation.js +16 -1
- package/src/core/templates.js +3 -0
- package/src/core/verification-capability.js +31 -495
- package/src/core/verification-constants.js +61 -0
package/src/core/preflight.js
CHANGED
|
@@ -1,205 +1,44 @@
|
|
|
1
|
-
import { ARTIFACT_PATHS,
|
|
1
|
+
import { ARTIFACT_PATHS, writeJsonArtifact } from "./artifacts.js";
|
|
2
2
|
import { readContract } from "./contract.js";
|
|
3
|
-
import { readGateIfPresent, validateGateArtifacts } from "./gate-artifact.js";
|
|
4
|
-
import { requiredGatesForGuides } from "./guide-metadata.js";
|
|
5
3
|
import { assertRouteInvariants } from "./router.js";
|
|
6
|
-
import {
|
|
7
|
-
import { readPersistedRoute } from "./route-artifact.js";
|
|
8
|
-
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
4
|
+
import { validateProfileSources } from "./profile.js";
|
|
9
5
|
import { readWorkState } from "./work-state.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
import { ensureResumableState, synchronizePreflightState } from "./preflight-consistency.js";
|
|
7
|
+
import {
|
|
8
|
+
issue,
|
|
9
|
+
preflightError,
|
|
10
|
+
sortIssues,
|
|
11
|
+
validatePersistedPreflight,
|
|
12
|
+
} from "./preflight-model.js";
|
|
13
|
+
import {
|
|
14
|
+
loadContract,
|
|
15
|
+
loadRoute,
|
|
16
|
+
loadSources,
|
|
17
|
+
inspectGates,
|
|
18
|
+
optionalConfig,
|
|
19
|
+
readProfile,
|
|
20
|
+
} from "./preflight-loaders.js";
|
|
21
|
+
import {
|
|
22
|
+
appendActivationEvents,
|
|
23
|
+
assertExistingReadyLifecycleCompatibility,
|
|
24
|
+
assertPreflightPersistenceSafety,
|
|
25
|
+
assertPreflightResultPersistenceSafety,
|
|
26
|
+
validateReadyProtocolConsistency as validateReadyProtocolConsistencyCore,
|
|
27
|
+
} from "./preflight-consistency.js";
|
|
16
28
|
import { PROFILE_PATH } from "./target-layout.js";
|
|
29
|
+
import { readPersistedRoute } from "./route-artifact.js";
|
|
30
|
+
import { validateEventLedger } from "./events.js";
|
|
17
31
|
|
|
18
32
|
const PREVIEW_DECISION_LIMIT = 10;
|
|
19
33
|
const PREVIEW_DECISION_MAX_LENGTH = 240;
|
|
20
34
|
|
|
21
|
-
|
|
22
|
-
return { code, message, artifacts, ...details };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function sortIssues(errors) {
|
|
26
|
-
const unique = [...new Map(errors.map((error) => [
|
|
27
|
-
`${error.code}\0${error.artifacts.join("\0")}\0${error.message}`,
|
|
28
|
-
error,
|
|
29
|
-
])).values()];
|
|
30
|
-
return unique.sort((left, right) => left.code.localeCompare(right.code)
|
|
31
|
-
|| left.artifacts.join("\0").localeCompare(right.artifacts.join("\0"))
|
|
32
|
-
|| left.message.localeCompare(right.message));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function preflightError(code, message, artifacts = []) {
|
|
36
|
-
const error = new Error(message);
|
|
37
|
-
error.code = code;
|
|
38
|
-
error.artifacts = artifacts;
|
|
39
|
-
return error;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function sameStringSet(left, right) {
|
|
43
|
-
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
44
|
-
const leftSet = new Set(left);
|
|
45
|
-
const rightSet = new Set(right);
|
|
46
|
-
return leftSet.size === left.length
|
|
47
|
-
&& rightSet.size === right.length
|
|
48
|
-
&& leftSet.size === rightSet.size
|
|
49
|
-
&& [...leftSet].every((value) => rightSet.has(value));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function validatePersistedPreflight(persisted, current) {
|
|
53
|
-
const errors = [];
|
|
54
|
-
if (persisted?.status !== "READY") {
|
|
55
|
-
errors.push(issue("E_PREFLIGHT_NOT_READY", "A persisted READY preflight is required", [ARTIFACT_PATHS.preflight]));
|
|
56
|
-
return errors;
|
|
57
|
-
}
|
|
58
|
-
if (current?.status !== "READY") {
|
|
59
|
-
errors.push(issue("E_PREFLIGHT_NOT_READY", "The current preflight evaluation is not READY", [ARTIFACT_PATHS.preflight]));
|
|
60
|
-
}
|
|
61
|
-
if (persisted.taskId !== current?.taskId) {
|
|
62
|
-
errors.push(issue(
|
|
63
|
-
"E_PREFLIGHT_TASK_MISMATCH",
|
|
64
|
-
"Persisted preflight does not belong to the current task",
|
|
65
|
-
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.contract],
|
|
66
|
-
));
|
|
67
|
-
}
|
|
68
|
-
if (persisted.fingerprints?.contract !== current?.fingerprints?.contract
|
|
69
|
-
|| persisted.contract?.fingerprint !== current?.contract?.fingerprint) {
|
|
70
|
-
errors.push(issue(
|
|
71
|
-
"E_PREFLIGHT_CONTRACT_STALE",
|
|
72
|
-
"Persisted preflight does not match the current contract fingerprint",
|
|
73
|
-
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.contract],
|
|
74
|
-
));
|
|
75
|
-
}
|
|
76
|
-
if (persisted.fingerprints?.routing !== current?.fingerprints?.routing
|
|
77
|
-
|| persisted.routing?.fingerprint !== current?.routing?.fingerprint) {
|
|
78
|
-
errors.push(issue(
|
|
79
|
-
"E_PREFLIGHT_ROUTE_STALE",
|
|
80
|
-
"Persisted preflight does not match the current routing fingerprint",
|
|
81
|
-
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.route],
|
|
82
|
-
));
|
|
83
|
-
}
|
|
84
|
-
if (!sameStringSet(persisted.requiredGates, current?.requiredGates)
|
|
85
|
-
|| !sameStringSet(persisted.satisfiedGates, current?.satisfiedGates)) {
|
|
86
|
-
errors.push(issue(
|
|
87
|
-
"E_PREFLIGHT_GATES_STALE",
|
|
88
|
-
"Persisted preflight gate sets do not match the current evaluation",
|
|
89
|
-
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.gates],
|
|
90
|
-
));
|
|
91
|
-
}
|
|
92
|
-
return sortIssues(errors);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function readProfile(target) {
|
|
96
|
-
const relativePath = await findProfilePath(target);
|
|
97
|
-
if (!relativePath) return { status: "missing", fingerprint: null };
|
|
98
|
-
const filePath = ensureWithin(target, relativePath);
|
|
99
|
-
const bytes = await readBytes(filePath);
|
|
100
|
-
const text = bytes.toString("utf8");
|
|
101
|
-
const mode = text.match(/^profile-mode:\s*([^\s]+)\s*$/m)?.[1] ?? null;
|
|
102
|
-
const status = text.match(/^profile-status:\s*([^\s]+)\s*$/m)?.[1] ?? null;
|
|
103
|
-
return {
|
|
104
|
-
status: status === "verified" && mode !== "template" ? "verified" : "unverified",
|
|
105
|
-
mode,
|
|
106
|
-
profileStatus: status,
|
|
107
|
-
fingerprint: sha256(bytes),
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function optionalConfig(target, packageRoot, errors) {
|
|
112
|
-
try {
|
|
113
|
-
const artifact = await readJsonArtifact(target, ARTIFACT_PATHS.config, "config", packageRoot);
|
|
114
|
-
return artifact.value;
|
|
115
|
-
} catch (error) {
|
|
116
|
-
if (error.code === "ARTIFACT_MISSING") return { schemaVersion: 1, protocolVersion: 1, complianceMode: "standard" };
|
|
117
|
-
errors.push(issue("E_CONFIG_INVALID", error.message, [ARTIFACT_PATHS.config]));
|
|
118
|
-
return { schemaVersion: 1, protocolVersion: 1, complianceMode: "standard" };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function loadContract(target, packageRoot, errors) {
|
|
123
|
-
try {
|
|
124
|
-
return await readContract(target, packageRoot);
|
|
125
|
-
} catch (error) {
|
|
126
|
-
errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_CONTRACT_MISSING" : "E_CONTRACT_INVALID", error.message, [ARTIFACT_PATHS.contract]));
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function loadRoute(target, packageRoot, errors) {
|
|
132
|
-
try {
|
|
133
|
-
return await readPersistedRoute(target, packageRoot);
|
|
134
|
-
} catch (error) {
|
|
135
|
-
const code = error.code === "ARTIFACT_MISSING"
|
|
136
|
-
? "E_ROUTE_MISSING"
|
|
137
|
-
: ["E_ROUTE_REASON_MISSING", "E_ROUTE_INVALID"].includes(error.code) ? error.code : "E_ROUTE_INVALID";
|
|
138
|
-
errors.push(issue(code, error.message, [ARTIFACT_PATHS.route]));
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function loadSources(target, contract, packageRoot, errors) {
|
|
144
|
-
if (!contract?.value?.sourceRefs?.length) return null;
|
|
145
|
-
let registry;
|
|
146
|
-
try {
|
|
147
|
-
registry = (await readJsonArtifact(target, ARTIFACT_PATHS.sources, "source-registry", packageRoot)).value;
|
|
148
|
-
} catch (error) {
|
|
149
|
-
errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_PROFILE_SOURCE_MISSING" : "E_PROFILE_SOURCE_UNKNOWN", error.message, [ARTIFACT_PATHS.sources]));
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
try {
|
|
153
|
-
assertSourceProvenance(registry, contract.value.sourceRefs);
|
|
154
|
-
} catch (error) {
|
|
155
|
-
errors.push(issue(error.code ?? "E_PROFILE_SOURCE_UNKNOWN", error.message, [ARTIFACT_PATHS.sources]));
|
|
156
|
-
}
|
|
157
|
-
return registry;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function inspectGates(target, contract, route, packageRoot, errors, config = {}) {
|
|
161
|
-
if (!route) return { required: [], satisfied: [], records: {} };
|
|
162
|
-
const guideGates = await requiredGatesForGuides(route.value.guides, packageRoot);
|
|
163
|
-
const required = [...new Set([...guideGates, ...(config.requiredGates ?? [])])].sort();
|
|
164
|
-
const satisfied = [];
|
|
165
|
-
const records = {};
|
|
166
|
-
for (const gate of required) {
|
|
167
|
-
let artifact;
|
|
168
|
-
try {
|
|
169
|
-
artifact = await readGateIfPresent(target, gate, packageRoot);
|
|
170
|
-
} catch (error) {
|
|
171
|
-
errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_GATE_UNVERIFIED" : "E_GATE_INVALID", error.message, [`${ARTIFACT_PATHS.gates}/${gate}.json`], { gate }));
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
if (!artifact) {
|
|
175
|
-
errors.push(issue("E_GATE_UNVERIFIED", `Required gate is missing or unverified: ${gate}`, [`${ARTIFACT_PATHS.gates}/${gate}.json`], { gate }));
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
records[gate] = artifact;
|
|
179
|
-
if (artifact.value.taskId !== contract?.value?.taskId) {
|
|
180
|
-
errors.push(issue("E_GATE_TASK_MISMATCH", `Gate ${gate} belongs to a different task`, [artifact.path], { gate }));
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
if (artifact.value.status !== "satisfied") {
|
|
184
|
-
errors.push(issue("E_GATE_UNVERIFIED", `Required gate is ${artifact.value.status}: ${gate}`, [artifact.path], { gate }));
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
const stale = await validateGateArtifacts(target, artifact.value, packageRoot);
|
|
188
|
-
if (stale.length > 0) {
|
|
189
|
-
errors.push(issue("E_GATE_STALE", `Gate ${gate} references stale artifacts`, [artifact.path], { gate, stale }));
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
satisfied.push(gate);
|
|
193
|
-
}
|
|
194
|
-
return { required, satisfied: satisfied.sort(), records };
|
|
195
|
-
}
|
|
35
|
+
export { validatePersistedPreflight } from "./preflight-model.js";
|
|
196
36
|
|
|
197
37
|
export async function evaluatePreflight({ target, packageRoot, strict = false } = {}) {
|
|
198
38
|
const errors = [];
|
|
199
39
|
const profile = await readProfile(target);
|
|
200
40
|
const profileProvenance = await validateProfileSources(target, packageRoot);
|
|
201
|
-
|
|
202
|
-
errors.push(...provenanceErrors);
|
|
41
|
+
errors.push(...(profileProvenance.errors ?? []));
|
|
203
42
|
const contract = await loadContract(target, packageRoot, errors);
|
|
204
43
|
const route = await loadRoute(target, packageRoot, errors);
|
|
205
44
|
const config = await optionalConfig(target, packageRoot, errors);
|
|
@@ -207,6 +46,7 @@ export async function evaluatePreflight({ target, packageRoot, strict = false }
|
|
|
207
46
|
if (effectiveStrict && profile.status !== "verified") {
|
|
208
47
|
errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", [PROFILE_PATH]));
|
|
209
48
|
}
|
|
49
|
+
|
|
210
50
|
const unresolvedDecisions = contract?.value?.unresolvedDecisions ?? [];
|
|
211
51
|
if (unresolvedDecisions.length > 0) {
|
|
212
52
|
errors.push(issue(
|
|
@@ -281,261 +121,11 @@ export async function evaluatePreflight({ target, packageRoot, strict = false }
|
|
|
281
121
|
};
|
|
282
122
|
}
|
|
283
123
|
|
|
284
|
-
async function
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
throw preflightError(invalidCode, error.message, [artifactPath]);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
async function assertPreflightPersistenceSafety(target, packageRoot, taskId) {
|
|
294
|
-
let state;
|
|
295
|
-
try {
|
|
296
|
-
state = await readWorkState(target, packageRoot);
|
|
297
|
-
} catch (error) {
|
|
298
|
-
throw preflightError("E_STATE_INVALID", error.message, [ARTIFACT_PATHS.state]);
|
|
299
|
-
}
|
|
300
|
-
const contract = await readOptionalIdentityArtifact(
|
|
301
|
-
() => readContract(target, packageRoot),
|
|
302
|
-
"E_CONTRACT_INVALID",
|
|
303
|
-
ARTIFACT_PATHS.contract,
|
|
304
|
-
);
|
|
305
|
-
const route = await readOptionalIdentityArtifact(
|
|
306
|
-
() => readPersistedRoute(target, packageRoot),
|
|
307
|
-
"E_ROUTE_INVALID",
|
|
308
|
-
ARTIFACT_PATHS.route,
|
|
309
|
-
);
|
|
310
|
-
if (state) {
|
|
311
|
-
if (contract || route) assertStateIdentity({ contract, route, state });
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (taskId === "unknown") return null;
|
|
315
|
-
const ledger = await validateEventLedger(target, packageRoot);
|
|
316
|
-
if (!ledger.valid) {
|
|
317
|
-
const first = ledger.errors[0];
|
|
318
|
-
throw preflightError(first.code, first.message, [ARTIFACT_PATHS.events]);
|
|
319
|
-
}
|
|
320
|
-
if (ledger.events.some((event) => event.taskId !== taskId)) {
|
|
321
|
-
throw preflightError(
|
|
322
|
-
"E_PHASE_CHRONOLOGY_INVALID",
|
|
323
|
-
"Preflight cannot append events to a ledger owned by a different task",
|
|
324
|
-
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract],
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
return ledger;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const PREFLIGHT_IDENTITY_BARRIER_CODES = new Set([
|
|
331
|
-
"E_CONTRACT_STALE",
|
|
332
|
-
"E_GATE_TASK_MISMATCH",
|
|
333
|
-
"E_ROUTE_STALE",
|
|
334
|
-
"E_STATE_TASK_MISMATCH",
|
|
335
|
-
"E_ROUTE_GUIDE_MISMATCH",
|
|
336
|
-
]);
|
|
337
|
-
|
|
338
|
-
function assertPreflightResultPersistenceSafety(result) {
|
|
339
|
-
const identityError = result.errors.find((error) => PREFLIGHT_IDENTITY_BARRIER_CODES.has(error.code));
|
|
340
|
-
if (identityError) {
|
|
341
|
-
throw preflightError(identityError.code, identityError.message, identityError.artifacts);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function sameReadyPreflightEvent(event, result) {
|
|
346
|
-
return event.fingerprint === result.fingerprints.contract
|
|
347
|
-
&& event.details?.routingFingerprint === result.fingerprints.routing
|
|
348
|
-
&& sameStringSet(event.details?.requiredGates, result.requiredGates)
|
|
349
|
-
&& sameStringSet(event.details?.satisfiedGates, result.satisfiedGates);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export async function validateReadyProtocolConsistency({
|
|
353
|
-
target,
|
|
354
|
-
packageRoot,
|
|
355
|
-
persisted,
|
|
356
|
-
current = null,
|
|
357
|
-
} = {}) {
|
|
358
|
-
if (persisted?.status !== "READY") return [];
|
|
359
|
-
const result = current ?? await evaluatePreflight({ target, packageRoot });
|
|
360
|
-
const errors = [...validatePersistedPreflight(persisted, result)];
|
|
361
|
-
|
|
362
|
-
let state = null;
|
|
363
|
-
try {
|
|
364
|
-
state = await readWorkState(target, packageRoot);
|
|
365
|
-
} catch (error) {
|
|
366
|
-
errors.push(issue("E_STATE_INVALID", error.message, [ARTIFACT_PATHS.state]));
|
|
367
|
-
}
|
|
368
|
-
if (!state) {
|
|
369
|
-
errors.push(issue(
|
|
370
|
-
"E_STATE_MISSING_AFTER_PREFLIGHT_READY",
|
|
371
|
-
"A persisted READY preflight must have a resumable work-state checkpoint",
|
|
372
|
-
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.state],
|
|
373
|
-
));
|
|
374
|
-
} else {
|
|
375
|
-
if (state.taskId !== persisted.taskId) {
|
|
376
|
-
errors.push(issue(
|
|
377
|
-
"E_STATE_TASK_MISMATCH",
|
|
378
|
-
"The resumable checkpoint does not belong to the READY preflight task",
|
|
379
|
-
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.preflight],
|
|
380
|
-
));
|
|
381
|
-
}
|
|
382
|
-
if (state.contractFingerprint !== result.fingerprints.contract) {
|
|
383
|
-
errors.push(issue(
|
|
384
|
-
"E_CONTRACT_STALE",
|
|
385
|
-
"The resumable checkpoint does not match the READY contract fingerprint",
|
|
386
|
-
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract],
|
|
387
|
-
));
|
|
388
|
-
}
|
|
389
|
-
if (state.routeFingerprint !== result.fingerprints.routing) {
|
|
390
|
-
errors.push(issue(
|
|
391
|
-
"E_ROUTE_STALE",
|
|
392
|
-
"The resumable checkpoint does not match the READY routing fingerprint",
|
|
393
|
-
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.route],
|
|
394
|
-
));
|
|
395
|
-
}
|
|
396
|
-
if (JSON.stringify(state.selectedGuides) !== JSON.stringify(result.routing.guides)) {
|
|
397
|
-
errors.push(issue(
|
|
398
|
-
"E_ROUTE_GUIDE_MISMATCH",
|
|
399
|
-
"The resumable checkpoint guides do not match the READY routing result",
|
|
400
|
-
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.route],
|
|
401
|
-
));
|
|
402
|
-
}
|
|
403
|
-
if (!sameStringSet(state.requiredGates ?? [], persisted.requiredGates)
|
|
404
|
-
|| !sameStringSet(state.satisfiedGates ?? [], persisted.satisfiedGates)) {
|
|
405
|
-
errors.push(issue(
|
|
406
|
-
"E_PREFLIGHT_GATES_STALE",
|
|
407
|
-
"The resumable checkpoint gate sets do not match the READY preflight",
|
|
408
|
-
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.gates],
|
|
409
|
-
));
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const ledger = await validateEventLedger(target, packageRoot);
|
|
414
|
-
if (!ledger.valid) {
|
|
415
|
-
errors.push(...ledger.errors.map((error) => issue(
|
|
416
|
-
error.code ?? "E_EVENT_INVALID",
|
|
417
|
-
error.message,
|
|
418
|
-
[ARTIFACT_PATHS.events],
|
|
419
|
-
)));
|
|
420
|
-
}
|
|
421
|
-
const events = ledger.events ?? [];
|
|
422
|
-
for (const requiredEvent of ["CONTRACT_VALIDATED", "ROUTE_VALIDATED"]) {
|
|
423
|
-
if (!events.some((event) => event.event === requiredEvent && event.taskId === persisted.taskId)) {
|
|
424
|
-
errors.push(issue(
|
|
425
|
-
"E_PREFLIGHT_EVENT_MISSING",
|
|
426
|
-
`READY preflight is missing lifecycle event: ${requiredEvent}`,
|
|
427
|
-
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.preflight],
|
|
428
|
-
));
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
for (const gate of persisted.satisfiedGates ?? []) {
|
|
432
|
-
if (!events.some((event) => event.event === "GATE_SATISFIED"
|
|
433
|
-
&& event.taskId === persisted.taskId
|
|
434
|
-
&& event.details?.gate === gate)) {
|
|
435
|
-
errors.push(issue(
|
|
436
|
-
"E_PREFLIGHT_GATE_EVENT_MISSING",
|
|
437
|
-
`READY preflight is missing lifecycle gate event: ${gate}`,
|
|
438
|
-
[ARTIFACT_PATHS.events, `${ARTIFACT_PATHS.gates}/${gate}.json`],
|
|
439
|
-
));
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
const readyEvents = events.filter((event) => event.event === "PREFLIGHT_READY" && event.taskId === persisted.taskId);
|
|
443
|
-
if (readyEvents.length === 0) {
|
|
444
|
-
errors.push(issue(
|
|
445
|
-
"E_PREFLIGHT_READY_EVENT_MISSING",
|
|
446
|
-
"Persisted READY preflight is missing the matching PREFLIGHT_READY lifecycle event",
|
|
447
|
-
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events],
|
|
448
|
-
));
|
|
449
|
-
} else if (!readyEvents.some((event) => sameReadyPreflightEvent(event, result))) {
|
|
450
|
-
errors.push(issue(
|
|
451
|
-
"E_PREFLIGHT_READY_EVENT_MISMATCH",
|
|
452
|
-
"PREFLIGHT_READY lifecycle details do not match the persisted READY preflight",
|
|
453
|
-
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events],
|
|
454
|
-
));
|
|
455
|
-
}
|
|
456
|
-
return sortIssues(errors);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function sameBlockedPreflightEvent(event, result) {
|
|
460
|
-
return event.event === "PREFLIGHT_BLOCKED"
|
|
461
|
-
&& event.taskId === result.taskId
|
|
462
|
-
&& event.fingerprint === (result.fingerprints.contract ?? undefined)
|
|
463
|
-
&& sameStringSet(event.details?.requiredGates, result.requiredGates)
|
|
464
|
-
&& sameStringSet(event.details?.satisfiedGates, result.satisfiedGates)
|
|
465
|
-
&& event.details?.routingFingerprint === result.fingerprints.routing;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function assertExistingReadyLifecycleCompatibility(ledger, result) {
|
|
469
|
-
if (result.status !== "READY") return;
|
|
470
|
-
const existingReady = ledger?.events?.find((event) => event.event === "PREFLIGHT_READY" && event.taskId === result.taskId);
|
|
471
|
-
if (existingReady && !sameReadyPreflightEvent(existingReady, result)) {
|
|
472
|
-
throw preflightError(
|
|
473
|
-
"E_PHASE_CHRONOLOGY_INVALID",
|
|
474
|
-
"PREFLIGHT_READY already exists with different READY preflight details; repair the contract, route, or gate lifecycle before refreshing preflight",
|
|
475
|
-
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route, ARTIFACT_PATHS.gates],
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
async function appendActivationEvents(target, packageRoot, ledger, result) {
|
|
481
|
-
const events = [...(ledger?.events ?? [])];
|
|
482
|
-
const hasEvent = (eventName) => events.some((event) => event.event === eventName && event.taskId === result.taskId);
|
|
483
|
-
const append = async (input) => {
|
|
484
|
-
const event = await appendProtocolEvent(target, input, packageRoot);
|
|
485
|
-
events.push(event);
|
|
486
|
-
};
|
|
487
|
-
|
|
488
|
-
if (events.length === 0) {
|
|
489
|
-
await append({ taskId: result.taskId, event: "TASK_RECEIVED" });
|
|
490
|
-
}
|
|
491
|
-
if (result.contract.status === "valid" && !hasEvent("CONTRACT_VALIDATED")) {
|
|
492
|
-
await append({ taskId: result.taskId, event: "CONTRACT_VALIDATED", fingerprint: result.fingerprints.contract });
|
|
493
|
-
}
|
|
494
|
-
if (result.routing.status === "valid" && !hasEvent("ROUTE_VALIDATED")) {
|
|
495
|
-
await append({ taskId: result.taskId, event: "ROUTE_VALIDATED", fingerprint: result.fingerprints.routing });
|
|
496
|
-
}
|
|
497
|
-
for (const gate of result.satisfiedGates) {
|
|
498
|
-
if (!events.some((event) => event.event === "GATE_SATISFIED"
|
|
499
|
-
&& event.taskId === result.taskId
|
|
500
|
-
&& event.details?.gate === gate)) {
|
|
501
|
-
await append({ taskId: result.taskId, event: "GATE_SATISFIED", details: { gate } });
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const existingReady = events.find((event) => event.event === "PREFLIGHT_READY" && event.taskId === result.taskId);
|
|
506
|
-
if (result.status === "READY") {
|
|
507
|
-
if (existingReady && !sameReadyPreflightEvent(existingReady, result)) {
|
|
508
|
-
throw preflightError(
|
|
509
|
-
"E_PHASE_CHRONOLOGY_INVALID",
|
|
510
|
-
"PREFLIGHT_READY already exists with different READY preflight details; repair the contract, route, or gate lifecycle before refreshing preflight",
|
|
511
|
-
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route, ARTIFACT_PATHS.gates],
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
if (!existingReady) {
|
|
515
|
-
await append({
|
|
516
|
-
taskId: result.taskId,
|
|
517
|
-
event: "PREFLIGHT_READY",
|
|
518
|
-
fingerprint: result.fingerprints.contract ?? undefined,
|
|
519
|
-
details: {
|
|
520
|
-
requiredGates: result.requiredGates,
|
|
521
|
-
satisfiedGates: result.satisfiedGates,
|
|
522
|
-
routingFingerprint: result.fingerprints.routing,
|
|
523
|
-
},
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
} else if (!events.some((event) => sameBlockedPreflightEvent(event, result))) {
|
|
527
|
-
await append({
|
|
528
|
-
taskId: result.taskId,
|
|
529
|
-
event: "PREFLIGHT_BLOCKED",
|
|
530
|
-
fingerprint: result.fingerprints.contract ?? undefined,
|
|
531
|
-
details: {
|
|
532
|
-
requiredGates: result.requiredGates,
|
|
533
|
-
satisfiedGates: result.satisfiedGates,
|
|
534
|
-
routingFingerprint: result.fingerprints.routing,
|
|
535
|
-
},
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
return events;
|
|
124
|
+
export async function validateReadyProtocolConsistency(options = {}) {
|
|
125
|
+
return validateReadyProtocolConsistencyCore({
|
|
126
|
+
...options,
|
|
127
|
+
evaluateCurrentPreflight: evaluatePreflight,
|
|
128
|
+
});
|
|
539
129
|
}
|
|
540
130
|
|
|
541
131
|
export async function runPreflight({ target, packageRoot, strict = false, persist = true } = {}) {
|
package/src/core/protocol.js
CHANGED
|
@@ -86,6 +86,13 @@ export const FAILURE_CODES = Object.freeze([
|
|
|
86
86
|
"E_AUTHORITY_SCOPE_MISMATCH",
|
|
87
87
|
"E_AUTHORITY_UNTRUSTED_SOURCE",
|
|
88
88
|
"E_VERIFICATION_TOOL_UNAVAILABLE",
|
|
89
|
+
"E_CONTINUITY_INVALID",
|
|
90
|
+
"E_CONTINUITY_SCHEMA_UNSUPPORTED",
|
|
91
|
+
"E_CONTINUITY_STATE_MISSING",
|
|
92
|
+
"E_CONTINUITY_TASK_MISMATCH",
|
|
93
|
+
"E_CONTINUITY_CONTRACT_MISMATCH",
|
|
94
|
+
"E_CONTINUITY_PHASE_MISMATCH",
|
|
95
|
+
"E_CONTINUITY_RECONCILIATION_REQUIRED",
|
|
89
96
|
]);
|
|
90
97
|
|
|
91
98
|
export const WORK_PHASES = Object.freeze([
|
|
@@ -9,6 +9,7 @@ export const SHIPPED_SCHEMA_NAMES = Object.freeze([
|
|
|
9
9
|
"routing-input",
|
|
10
10
|
"routing-result",
|
|
11
11
|
"work-state",
|
|
12
|
+
"continuity",
|
|
12
13
|
"execution-receipt",
|
|
13
14
|
"task-brief",
|
|
14
15
|
"delegated-result",
|
|
@@ -24,6 +25,8 @@ export const SHIPPED_SCHEMA_NAMES = Object.freeze([
|
|
|
24
25
|
"activation",
|
|
25
26
|
"policy",
|
|
26
27
|
"task-bundle",
|
|
28
|
+
"execution",
|
|
29
|
+
"authority",
|
|
27
30
|
]);
|
|
28
31
|
|
|
29
32
|
export class SchemaValidationError extends Error {
|
|
@@ -160,10 +163,22 @@ export function assertSchema(value, schema, label = "value") {
|
|
|
160
163
|
return value;
|
|
161
164
|
}
|
|
162
165
|
|
|
166
|
+
const SCHEMA_CACHE = new Map();
|
|
167
|
+
|
|
168
|
+
export function clearSchemaCache() {
|
|
169
|
+
SCHEMA_CACHE.clear();
|
|
170
|
+
}
|
|
171
|
+
|
|
163
172
|
export async function readSchema(name, packageRoot = getPackageRoot()) {
|
|
164
173
|
const filename = name.endsWith(".schema.json") ? name : `${name}.schema.json`;
|
|
165
174
|
const schemaPath = path.join(packageRoot, "schemas", filename);
|
|
166
|
-
|
|
175
|
+
const cacheKey = `${packageRoot}:${filename}`;
|
|
176
|
+
if (SCHEMA_CACHE.has(cacheKey)) {
|
|
177
|
+
return SCHEMA_CACHE.get(cacheKey);
|
|
178
|
+
}
|
|
179
|
+
const schema = JSON.parse(await readFile(schemaPath, "utf8"));
|
|
180
|
+
SCHEMA_CACHE.set(cacheKey, schema);
|
|
181
|
+
return schema;
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
export async function inspectSchemaHealth(packageRoot = getPackageRoot()) {
|
package/src/core/templates.js
CHANGED
|
@@ -49,6 +49,7 @@ export const TEMPLATE_PATHS = [
|
|
|
49
49
|
"schemas/routing-input.schema.json",
|
|
50
50
|
"schemas/routing-result.schema.json",
|
|
51
51
|
"schemas/work-state.schema.json",
|
|
52
|
+
"schemas/continuity.schema.json",
|
|
52
53
|
"schemas/execution-receipt.schema.json",
|
|
53
54
|
"schemas/task-brief.schema.json",
|
|
54
55
|
"schemas/delegated-result.schema.json",
|
|
@@ -59,11 +60,13 @@ export const TEMPLATE_PATHS = [
|
|
|
59
60
|
"schemas/config.schema.json",
|
|
60
61
|
"schemas/preflight.schema.json",
|
|
61
62
|
"schemas/check.schema.json",
|
|
63
|
+
"schemas/execution.schema.json",
|
|
62
64
|
"schemas/evidence-coverage.schema.json",
|
|
63
65
|
"schemas/event.schema.json",
|
|
64
66
|
"schemas/activation.schema.json",
|
|
65
67
|
"schemas/policy.schema.json",
|
|
66
68
|
"schemas/task-bundle.schema.json",
|
|
69
|
+
"schemas/authority.schema.json",
|
|
67
70
|
];
|
|
68
71
|
|
|
69
72
|
export function getPackageRoot() {
|