@cassiomc1/forgeloop 1.0.0 → 1.1.0
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 +3 -2
- package/.github/copilot-instructions.md +1 -0
- package/AGENTS.md +2 -1
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +36 -0
- package/ENG/design-code-eng.md +31 -0
- package/ENG/documentation-quality-eng.md +400 -0
- package/EXECUTION_STATE.md +23 -13
- package/GUIDE_ROUTER.md +23 -8
- package/LOOP_ENGINEERING.md +77 -12
- package/PROTOCOL_INTEGRATION.md +9 -6
- package/README.md +82 -39
- package/docs/ARTIFACT_REFERENCE.md +442 -0
- package/docs/CLI_REFERENCE.md +892 -0
- package/docs/CROSS_HARNESS_CONTINUITY.md +198 -0
- package/docs/DOCUMENTATION_GUIDE.md +161 -0
- package/docs/GETTING_STARTED.md +348 -0
- package/docs/RECIPES.md +250 -0
- package/docs/TROUBLESHOOTING.md +345 -0
- package/docs/assets/forgeloop-flow.svg +1 -1
- package/docs/forgeloop-flow.mmd +1 -1
- package/package.json +13 -2
- package/schemas/task-descriptor.schema.json +56 -0
- package/schemas/work-state.schema.json +18 -1
- package/scripts/CI_VALIDATORS.md +7 -0
- package/src/cli.js +280 -388
- package/src/commands/advance.js +5 -2
- package/src/commands/audit.js +11 -1
- package/src/commands/clear-continuity.js +5 -2
- package/src/commands/clear-state.js +5 -2
- package/src/commands/complete.js +9 -1
- package/src/commands/continuity.js +5 -2
- package/src/commands/inspect.js +10 -2
- package/src/commands/next.js +5 -2
- package/src/commands/preflight.js +9 -1
- package/src/commands/prepare-completion.js +5 -2
- package/src/commands/reconcile-continuity.js +5 -2
- package/src/commands/record-check.js +7 -1
- package/src/commands/record-continuity.js +21 -14
- package/src/commands/record-terminal-result.js +7 -1
- package/src/commands/route.js +22 -18
- package/src/commands/run-check.js +52 -44
- package/src/commands/status.js +18 -12
- package/src/commands/task-create.js +94 -0
- package/src/commands/task-list.js +48 -0
- package/src/commands/task-migrate.js +34 -0
- package/src/commands/task-scope.js +75 -0
- package/src/commands/task-show.js +81 -0
- package/src/commands/task-unlock.js +35 -0
- package/src/commands/validate-protocol.js +37 -20
- package/src/commands/validate-state.js +24 -18
- package/src/config/guides.json +42 -0
- package/src/core/activation.js +8 -4
- package/src/core/artifact-registry.js +166 -0
- package/src/core/audit.js +65 -12
- package/src/core/bundles.js +76 -50
- package/src/core/cli-command-definitions.js +611 -0
- package/src/core/cli-metadata.js +23 -0
- package/src/core/completion-artifacts.js +161 -74
- package/src/core/completion.js +134 -76
- package/src/core/continuity.js +20 -13
- package/src/core/contract.js +6 -3
- package/src/core/error-codes.js +197 -0
- package/src/core/events.js +19 -14
- package/src/core/execution.js +38 -6
- package/src/core/gate-artifact.js +12 -9
- package/src/core/gates.js +4 -2
- package/src/core/guide-metadata.js +7 -11
- package/src/core/guide-registry.js +29 -0
- package/src/core/inspect.js +7 -4
- package/src/core/native-adapters.js +6 -0
- package/src/core/phase.js +85 -33
- package/src/core/preflight-consistency.js +24 -14
- package/src/core/preflight-loaders.js +16 -11
- package/src/core/preflight.js +44 -25
- package/src/core/protocol.js +2 -11
- package/src/core/receipt.js +1 -1
- package/src/core/report.js +2 -2
- package/src/core/repository.js +46 -12
- package/src/core/resumability.js +6 -4
- package/src/core/route-artifact.js +9 -5
- package/src/core/router.js +11 -7
- package/src/core/schema-validation.js +1 -0
- package/src/core/task-command.js +41 -0
- package/src/core/task-context.js +126 -0
- package/src/core/task-descriptor.js +81 -0
- package/src/core/task-discovery.js +116 -0
- package/src/core/task-identity.js +76 -0
- package/src/core/task-lock.js +209 -0
- package/src/core/task-migration-validation.js +140 -0
- package/src/core/task-migration.js +361 -0
- package/src/core/task-paths.js +96 -0
- package/src/core/task-scope.js +179 -0
- package/src/core/templates.js +3 -9
- package/src/core/work-state.js +24 -13
package/src/core/phase.js
CHANGED
|
@@ -11,11 +11,16 @@ import { readPersistedRoute } from "./route-artifact.js";
|
|
|
11
11
|
import { assertWorkPhase, isValidTransition } from "./protocol.js";
|
|
12
12
|
import { readWorkState, writeWorkState } from "./work-state.js";
|
|
13
13
|
import { evaluateCompletion } from "./completion.js";
|
|
14
|
-
import { evaluatePreflight
|
|
14
|
+
import { evaluatePreflight } from "./preflight.js";
|
|
15
15
|
import { requiredEvidenceForTarget } from "./completion-artifacts.js";
|
|
16
16
|
import { assertCompletionRelationships, assertStateIdentity } from "./completion-relationships.js";
|
|
17
17
|
import { createReceipt, validateReceipt } from "./receipt.js";
|
|
18
18
|
import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
|
|
19
|
+
import { taskArtifactPath } from "./task-paths.js";
|
|
20
|
+
import { discoverTasks } from "./task-discovery.js";
|
|
21
|
+
import { assertNoScopeConflicts, assertScopeClean } from "./task-scope.js";
|
|
22
|
+
import { readTaskDescriptor } from "./task-descriptor.js";
|
|
23
|
+
import { E_TASK_SCOPE_REQUIRED } from "./error-codes.js";
|
|
19
24
|
|
|
20
25
|
function phaseError(code, message, artifacts = []) {
|
|
21
26
|
const error = new Error(message);
|
|
@@ -44,15 +49,19 @@ const LATE_PHASES = new Set([
|
|
|
44
49
|
"COMPLETE",
|
|
45
50
|
]);
|
|
46
51
|
|
|
47
|
-
async function assertPersistedStateIdentity(target, state, toPhase, packageRoot) {
|
|
52
|
+
async function assertPersistedStateIdentity(target, state, toPhase, packageRoot, options = {}) {
|
|
53
|
+
const scopedTaskId = options.taskId ?? null;
|
|
48
54
|
const requireContract = LATE_PHASES.has(state.phase)
|
|
49
55
|
|| ["CONTRACT_READY", "ROUTED", "EXECUTING"].includes(toPhase);
|
|
50
56
|
const requireRoute = LATE_PHASES.has(state.phase)
|
|
51
57
|
|| ["ROUTED", "EXECUTING"].includes(toPhase);
|
|
58
|
+
const contractRel = options.contractPath ?? (scopedTaskId ? taskArtifactPath(scopedTaskId, "contract") : ARTIFACT_PATHS.contract);
|
|
59
|
+
const routeRel = options.routePath ?? (scopedTaskId ? taskArtifactPath(scopedTaskId, "route") : ARTIFACT_PATHS.route);
|
|
60
|
+
|
|
52
61
|
let contract = null;
|
|
53
62
|
let route = null;
|
|
54
63
|
try {
|
|
55
|
-
contract = await readContract(target, packageRoot);
|
|
64
|
+
contract = await readContract(target, packageRoot, { taskId: scopedTaskId, contractPath: options.contractPath });
|
|
56
65
|
} catch (error) {
|
|
57
66
|
if (error.code === "ARTIFACT_MISSING" && !requireContract) {
|
|
58
67
|
contract = null;
|
|
@@ -60,12 +69,12 @@ async function assertPersistedStateIdentity(target, state, toPhase, packageRoot)
|
|
|
60
69
|
throw phaseError(
|
|
61
70
|
requireContract ? "E_PHASE_PREREQUISITE_MISSING" : error.code ?? "E_CONTRACT_INVALID",
|
|
62
71
|
`${requireContract ? `Phase ${toPhase} requires current contract` : "Unable to validate current contract"}: ${error.message}`,
|
|
63
|
-
[
|
|
72
|
+
[contractRel],
|
|
64
73
|
);
|
|
65
74
|
}
|
|
66
75
|
}
|
|
67
76
|
try {
|
|
68
|
-
route = await readPersistedRoute(target, packageRoot);
|
|
77
|
+
route = await readPersistedRoute(target, packageRoot, { taskId: scopedTaskId, routePath: options.routePath });
|
|
69
78
|
} catch (error) {
|
|
70
79
|
if (error.code === "ARTIFACT_MISSING" && !requireRoute) {
|
|
71
80
|
route = null;
|
|
@@ -73,7 +82,7 @@ async function assertPersistedStateIdentity(target, state, toPhase, packageRoot)
|
|
|
73
82
|
throw phaseError(
|
|
74
83
|
requireRoute ? "E_PHASE_PREREQUISITE_MISSING" : error.code ?? "E_ROUTE_INVALID",
|
|
75
84
|
`${requireRoute ? `Phase ${toPhase} requires persisted route` : "Unable to validate persisted route"}: ${error.message}`,
|
|
76
|
-
[
|
|
85
|
+
[routeRel],
|
|
77
86
|
);
|
|
78
87
|
}
|
|
79
88
|
}
|
|
@@ -101,24 +110,28 @@ function reconcileImplementationStep(state, toPhase) {
|
|
|
101
110
|
};
|
|
102
111
|
}
|
|
103
112
|
|
|
104
|
-
async function assertPhasePrerequisites(target, state, toPhase, packageRoot, authorityContext, runtimeContext) {
|
|
113
|
+
async function assertPhasePrerequisites(target, state, toPhase, packageRoot, authorityContext, runtimeContext, options = {}) {
|
|
114
|
+
const scopedTaskId = options.taskId ?? null;
|
|
115
|
+
const contractRel = options.contractPath ?? (scopedTaskId ? taskArtifactPath(scopedTaskId, "contract") : ARTIFACT_PATHS.contract);
|
|
116
|
+
const routeRel = options.routePath ?? (scopedTaskId ? taskArtifactPath(scopedTaskId, "route") : ARTIFACT_PATHS.route);
|
|
117
|
+
|
|
105
118
|
if (toPhase === "CONTRACT_READY" || toPhase === "ROUTED" || toPhase === "EXECUTING") {
|
|
106
119
|
try {
|
|
107
|
-
await readContract(target, packageRoot);
|
|
120
|
+
await readContract(target, packageRoot, { taskId: scopedTaskId, contractPath: options.contractPath });
|
|
108
121
|
} catch (error) {
|
|
109
|
-
throw phaseError("E_PHASE_PREREQUISITE_MISSING", `Phase ${toPhase} requires ${
|
|
122
|
+
throw phaseError("E_PHASE_PREREQUISITE_MISSING", `Phase ${toPhase} requires ${contractRel}: ${error.message}`, [contractRel]);
|
|
110
123
|
}
|
|
111
124
|
}
|
|
112
125
|
if (toPhase === "ROUTED" || toPhase === "EXECUTING") {
|
|
113
126
|
try {
|
|
114
|
-
await readPersistedRoute(target, packageRoot);
|
|
127
|
+
await readPersistedRoute(target, packageRoot, { taskId: scopedTaskId, routePath: options.routePath });
|
|
115
128
|
} catch (error) {
|
|
116
|
-
throw phaseError("E_PHASE_PREREQUISITE_MISSING", `Phase ${toPhase} requires ${
|
|
129
|
+
throw phaseError("E_PHASE_PREREQUISITE_MISSING", `Phase ${toPhase} requires ${routeRel}: ${error.message}`, [routeRel]);
|
|
117
130
|
}
|
|
118
131
|
}
|
|
119
132
|
if (hasExecutionStarted(toPhase)) {
|
|
120
133
|
try {
|
|
121
|
-
await assertExecutionPrerequisites({ target, state, packageRoot });
|
|
134
|
+
await assertExecutionPrerequisites({ target, state, packageRoot, ...options, taskId: scopedTaskId });
|
|
122
135
|
} catch (error) {
|
|
123
136
|
throw phaseError(error.code, error.message, error.artifacts);
|
|
124
137
|
}
|
|
@@ -127,7 +140,7 @@ async function assertPhasePrerequisites(target, state, toPhase, packageRoot, aut
|
|
|
127
140
|
throw phaseError("E_PHASE_EVIDENCE_MISSING", "COMPLETE requires verification evidence");
|
|
128
141
|
}
|
|
129
142
|
if (toPhase === "COMPLETE") {
|
|
130
|
-
const completion = await evaluateCompletion({ target, packageRoot, persist: false, authorityContext, runtimeContext });
|
|
143
|
+
const completion = await evaluateCompletion({ target, packageRoot, persist: false, authorityContext, runtimeContext, taskId: scopedTaskId, ...options });
|
|
131
144
|
if (completion.status !== "VALID") {
|
|
132
145
|
throw phaseError("E_COMPLETION_REJECTED", "COMPLETE requires a valid completion audit", completion.errors.flatMap((error) => error.artifacts ?? []));
|
|
133
146
|
}
|
|
@@ -141,37 +154,76 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
141
154
|
now = new Date().toISOString(),
|
|
142
155
|
authorityContext,
|
|
143
156
|
runtimeContext,
|
|
157
|
+
taskId = null,
|
|
158
|
+
statePath = null,
|
|
159
|
+
contractPath = null,
|
|
160
|
+
routePath = null,
|
|
161
|
+
receiptPath = null,
|
|
162
|
+
eventsPath = null,
|
|
144
163
|
} = normalizedOptions;
|
|
145
164
|
assertWorkPhase(toPhase);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
165
|
+
|
|
166
|
+
const state = await readWorkState(target, { packageRoot, taskId, statePath });
|
|
167
|
+
const stateRel = statePath ?? (taskId ? taskArtifactPath(taskId, "state") : ARTIFACT_PATHS.state);
|
|
168
|
+
if (!state) throw phaseError("E_PHASE_PREREQUISITE_MISSING", "Cannot advance without work state", [stateRel]);
|
|
169
|
+
|
|
170
|
+
const eventsRel = eventsPath ?? (taskId ? taskArtifactPath(taskId, "events") : ARTIFACT_PATHS.events);
|
|
171
|
+
const receiptRel = receiptPath ?? (taskId ? taskArtifactPath(taskId, "receipt") : ARTIFACT_PATHS.receipt);
|
|
172
|
+
|
|
173
|
+
await assertPhasePrerequisites(target, state, toPhase, packageRoot, authorityContext, runtimeContext, { taskId, statePath, contractPath, routePath, receiptPath, eventsPath });
|
|
174
|
+
await assertPersistedStateIdentity(target, state, toPhase, packageRoot, { taskId, contractPath, routePath });
|
|
175
|
+
|
|
176
|
+
if (toPhase === "EXECUTING" && taskId) {
|
|
177
|
+
// Multi-task checkout scope checks
|
|
178
|
+
const discovered = await discoverTasks(target, packageRoot);
|
|
179
|
+
let descriptor = null;
|
|
180
|
+
try {
|
|
181
|
+
const descArtifact = await readTaskDescriptor(target, taskId, packageRoot);
|
|
182
|
+
descriptor = descArtifact.value;
|
|
183
|
+
} catch {
|
|
184
|
+
// Descriptor might not exist if legacy
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const nonCompleteTasks = discovered.filter((t) => t.phase !== "COMPLETE");
|
|
188
|
+
if (nonCompleteTasks.length > 1) {
|
|
189
|
+
const claims = descriptor?.writeClaims ?? [];
|
|
190
|
+
if (claims.length === 0) {
|
|
191
|
+
throw phaseError(
|
|
192
|
+
E_TASK_SCOPE_REQUIRED,
|
|
193
|
+
"Multiple tasks exist in the repository. Entering EXECUTING requires declared non-empty write claims. Use 'forgeloop task-scope' first.",
|
|
194
|
+
[descriptor ? taskArtifactPath(taskId, "descriptor") : stateRel],
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
assertNoScopeConflicts(claims, discovered, taskId);
|
|
198
|
+
await assertScopeClean(target, claims);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
150
202
|
if (!isValidTransition(state.phase, toPhase)) {
|
|
151
203
|
throw phaseError("E_PHASE_TRANSITION_INVALID", `Invalid work-state transition: ${state.phase} -> ${toPhase}`);
|
|
152
204
|
}
|
|
153
|
-
const ledger = await validateEventLedger(target, packageRoot);
|
|
205
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId, eventsPath });
|
|
154
206
|
if (!ledger.valid) {
|
|
155
207
|
const first = ledger.errors[0];
|
|
156
|
-
throw phaseError(first.code, first.message, [
|
|
208
|
+
throw phaseError(first.code, first.message, [eventsRel]);
|
|
157
209
|
}
|
|
158
210
|
if (ledger.events.some((event) => event.taskId !== state.taskId)) {
|
|
159
211
|
throw phaseError(
|
|
160
212
|
"E_PHASE_CHRONOLOGY_INVALID",
|
|
161
213
|
"Cannot advance work state with lifecycle events from a different task",
|
|
162
|
-
[
|
|
214
|
+
[eventsRel, stateRel],
|
|
163
215
|
);
|
|
164
216
|
}
|
|
165
217
|
const coherenceErrors = validateStateLedgerCoherence(state, ledger.events);
|
|
166
218
|
if (coherenceErrors.length > 0) {
|
|
167
|
-
throw phaseError(coherenceErrors[0].code, coherenceErrors[0].message, [
|
|
219
|
+
throw phaseError(coherenceErrors[0].code, coherenceErrors[0].message, [stateRel, eventsRel]);
|
|
168
220
|
}
|
|
169
221
|
const eventType = PHASE_EVENTS[toPhase];
|
|
170
222
|
const reenteringVerification = toPhase === "VERIFYING" && ["CORRECTING", "REVIEWING"].includes(state.phase);
|
|
171
223
|
if (toPhase === "VERIFYING" && state.phase === "REVIEWING") {
|
|
172
224
|
let currentReceipt = null;
|
|
173
225
|
try {
|
|
174
|
-
const receiptArtifact = await readJsonArtifact(target,
|
|
226
|
+
const receiptArtifact = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
|
|
175
227
|
currentReceipt = receiptArtifact?.value;
|
|
176
228
|
} catch {
|
|
177
229
|
// If receipt is not present, pass null
|
|
@@ -186,7 +238,7 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
186
238
|
throw phaseError(
|
|
187
239
|
firstError.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
|
|
188
240
|
firstError.message ?? "REVIEWING -> VERIFYING requires authorized completion recovery",
|
|
189
|
-
[
|
|
241
|
+
[stateRel, eventsRel],
|
|
190
242
|
);
|
|
191
243
|
}
|
|
192
244
|
}
|
|
@@ -195,7 +247,7 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
195
247
|
throw phaseError(
|
|
196
248
|
"E_PHASE_PREREQUISITE_MISSING",
|
|
197
249
|
"CORRECTING -> VERIFYING requires a diagnosed hypothesis",
|
|
198
|
-
[
|
|
250
|
+
[stateRel],
|
|
199
251
|
);
|
|
200
252
|
}
|
|
201
253
|
}
|
|
@@ -203,13 +255,13 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
203
255
|
&& ledger.events.some((event) => event.taskId === state.taskId && event.event === "REVIEW_STARTED");
|
|
204
256
|
if (eventType && !reenteringVerification && !repeatedReview
|
|
205
257
|
&& ledger.events.some((event) => event.taskId === state.taskId && event.event === eventType)) {
|
|
206
|
-
throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `Lifecycle milestone already exists: ${eventType}`, [
|
|
258
|
+
throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `Lifecycle milestone already exists: ${eventType}`, [eventsRel]);
|
|
207
259
|
}
|
|
208
260
|
const milestoneIndex = reenteringVerification || repeatedReview ? -1 : LIFECYCLE_MILESTONES.indexOf(eventType);
|
|
209
261
|
if (milestoneIndex >= 0) {
|
|
210
262
|
const lastMilestone = ledger.events.reduce((last, event) => Math.max(last, LIFECYCLE_MILESTONES.indexOf(event.event)), -1);
|
|
211
263
|
if (lastMilestone !== milestoneIndex - 1) {
|
|
212
|
-
throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `Phase ${toPhase} cannot append ${eventType} after the current lifecycle ledger`, [
|
|
264
|
+
throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `Phase ${toPhase} cannot append ${eventType} after the current lifecycle ledger`, [eventsRel]);
|
|
213
265
|
}
|
|
214
266
|
}
|
|
215
267
|
const reconciled = reconcileImplementationStep(state, toPhase);
|
|
@@ -225,10 +277,10 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
225
277
|
if (reenteringVerification) delete next.lastCompletionAttempt;
|
|
226
278
|
let nextReceipt = null;
|
|
227
279
|
try {
|
|
228
|
-
const receipt = await readJsonArtifact(target,
|
|
229
|
-
const contract = await readContract(target, packageRoot);
|
|
230
|
-
const route = await readPersistedRoute(target, packageRoot);
|
|
231
|
-
const preflight = await evaluatePreflight({ target, packageRoot });
|
|
280
|
+
const receipt = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
|
|
281
|
+
const contract = await readContract(target, packageRoot, { taskId, contractPath });
|
|
282
|
+
const route = await readPersistedRoute(target, packageRoot, { taskId, routePath });
|
|
283
|
+
const preflight = await evaluatePreflight({ target, packageRoot, taskId, contractPath, routePath, statePath });
|
|
232
284
|
const requiredEvidence = await requiredEvidenceForTarget({
|
|
233
285
|
target,
|
|
234
286
|
contract,
|
|
@@ -280,9 +332,9 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
280
332
|
} catch (error) {
|
|
281
333
|
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
282
334
|
}
|
|
283
|
-
await writeWorkState(target, next, { packageRoot });
|
|
335
|
+
await writeWorkState(target, next, { packageRoot, taskId, statePath });
|
|
284
336
|
if (nextReceipt) {
|
|
285
|
-
await writeJsonArtifact(target,
|
|
337
|
+
await writeJsonArtifact(target, receiptRel, nextReceipt, "execution-receipt", packageRoot);
|
|
286
338
|
}
|
|
287
339
|
if (eventType) await appendProtocolEvent(target, {
|
|
288
340
|
taskId: state.taskId,
|
|
@@ -293,6 +345,6 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
293
345
|
: eventType === "REVIEW_STARTED"
|
|
294
346
|
? { verificationCycle: next.verificationCycle ?? 1 }
|
|
295
347
|
: undefined,
|
|
296
|
-
}, packageRoot);
|
|
348
|
+
}, packageRoot, { taskId, eventsPath });
|
|
297
349
|
return next;
|
|
298
350
|
}
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
validatePersistedPreflight,
|
|
15
15
|
} from "./preflight-model.js";
|
|
16
16
|
|
|
17
|
+
import { taskArtifactPath } from "./task-paths.js";
|
|
18
|
+
|
|
17
19
|
async function readOptionalIdentityArtifact(readArtifact, invalidCode, artifactPath) {
|
|
18
20
|
try {
|
|
19
21
|
return await readArtifact();
|
|
@@ -23,36 +25,44 @@ async function readOptionalIdentityArtifact(readArtifact, invalidCode, artifactP
|
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
export async function assertPreflightPersistenceSafety(target, packageRoot,
|
|
28
|
+
export async function assertPreflightPersistenceSafety(target, packageRoot, taskIdOrOptions = {}, maybeOptions = {}) {
|
|
29
|
+
const options = typeof taskIdOrOptions === "object" && taskIdOrOptions !== null ? taskIdOrOptions : maybeOptions;
|
|
30
|
+
const taskId = typeof taskIdOrOptions === "string" ? taskIdOrOptions : (options.taskId ?? null);
|
|
31
|
+
const contractRel = options.contractPath ?? (taskId && taskId !== "unknown" ? taskArtifactPath(taskId, "contract") : ARTIFACT_PATHS.contract);
|
|
32
|
+
const routeRel = options.routePath ?? (taskId && taskId !== "unknown" ? taskArtifactPath(taskId, "route") : ARTIFACT_PATHS.route);
|
|
33
|
+
const stateRel = options.statePath ?? (taskId && taskId !== "unknown" ? taskArtifactPath(taskId, "state") : ARTIFACT_PATHS.state);
|
|
34
|
+
const eventsRel = options.eventsPath ?? (taskId && taskId !== "unknown" ? taskArtifactPath(taskId, "events") : ARTIFACT_PATHS.events);
|
|
35
|
+
|
|
27
36
|
let state;
|
|
28
37
|
try {
|
|
29
|
-
state = await readWorkState(target, packageRoot);
|
|
38
|
+
state = await readWorkState(target, { packageRoot, taskId, statePath: options.statePath });
|
|
30
39
|
} catch (error) {
|
|
31
|
-
throw preflightError("E_STATE_INVALID", error.message, [
|
|
40
|
+
throw preflightError("E_STATE_INVALID", error.message, [stateRel]);
|
|
32
41
|
}
|
|
33
42
|
const contract = await readOptionalIdentityArtifact(
|
|
34
|
-
() => readContract(target, packageRoot),
|
|
43
|
+
() => readContract(target, packageRoot, { taskId, contractPath: options.contractPath }),
|
|
35
44
|
"E_CONTRACT_INVALID",
|
|
36
|
-
|
|
45
|
+
contractRel,
|
|
37
46
|
);
|
|
38
47
|
const route = await readOptionalIdentityArtifact(
|
|
39
|
-
() => readPersistedRoute(target, packageRoot),
|
|
48
|
+
() => readPersistedRoute(target, packageRoot, { taskId, routePath: options.routePath }),
|
|
40
49
|
"E_ROUTE_INVALID",
|
|
41
|
-
|
|
50
|
+
routeRel,
|
|
42
51
|
);
|
|
43
52
|
if (state && (contract || route)) assertStateIdentity({ contract, route, state });
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
const effectiveTaskId = taskId ?? contract?.value?.taskId ?? state?.taskId ?? null;
|
|
55
|
+
if (effectiveTaskId === "unknown" || !effectiveTaskId) return null;
|
|
56
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId, eventsPath: options.eventsPath });
|
|
47
57
|
if (!ledger.valid) {
|
|
48
58
|
const first = ledger.errors[0];
|
|
49
|
-
throw preflightError(first.code, first.message, [
|
|
59
|
+
throw preflightError(first.code, first.message, [eventsRel]);
|
|
50
60
|
}
|
|
51
|
-
if (ledger.events.some((event) => event.taskId !==
|
|
61
|
+
if (ledger.events.some((event) => event.taskId !== effectiveTaskId)) {
|
|
52
62
|
throw preflightError(
|
|
53
63
|
"E_PHASE_CHRONOLOGY_INVALID",
|
|
54
64
|
"Preflight cannot append events to a ledger owned by a different task",
|
|
55
|
-
[
|
|
65
|
+
[eventsRel, contractRel],
|
|
56
66
|
);
|
|
57
67
|
}
|
|
58
68
|
return ledger;
|
|
@@ -159,11 +169,11 @@ export function assertExistingReadyLifecycleCompatibility(ledger, result) {
|
|
|
159
169
|
}
|
|
160
170
|
}
|
|
161
171
|
|
|
162
|
-
export async function appendActivationEvents(target, packageRoot, ledger, result) {
|
|
172
|
+
export async function appendActivationEvents(target, packageRoot, ledger, result, options = {}) {
|
|
163
173
|
const events = [...(ledger?.events ?? [])];
|
|
164
174
|
const hasEvent = (eventName) => events.some((event) => event.event === eventName && event.taskId === result.taskId);
|
|
165
175
|
const append = async (input) => {
|
|
166
|
-
const event = await appendProtocolEvent(target, input, packageRoot);
|
|
176
|
+
const event = await appendProtocolEvent(target, input, packageRoot, options);
|
|
167
177
|
events.push(event);
|
|
168
178
|
};
|
|
169
179
|
|
|
@@ -6,8 +6,9 @@ import { assertSourceProvenance } from "./sources.js";
|
|
|
6
6
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
7
7
|
import { ensureWithin, readBytes } from "./filesystem.js";
|
|
8
8
|
import { sha256 } from "./manifest.js";
|
|
9
|
-
import { findProfilePath
|
|
9
|
+
import { findProfilePath } from "./profile.js";
|
|
10
10
|
import { issue } from "./preflight-model.js";
|
|
11
|
+
import { taskGatePath, taskArtifactPath } from "./task-paths.js";
|
|
11
12
|
|
|
12
13
|
export async function readProfile(target) {
|
|
13
14
|
const relativePath = await findProfilePath(target);
|
|
@@ -36,23 +37,25 @@ export async function optionalConfig(target, packageRoot, errors) {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
export async function loadContract(target, packageRoot, errors) {
|
|
40
|
+
export async function loadContract(target, packageRoot, errors, options = {}) {
|
|
40
41
|
try {
|
|
41
|
-
return await readContract(target, packageRoot);
|
|
42
|
+
return await readContract(target, packageRoot, options);
|
|
42
43
|
} catch (error) {
|
|
43
|
-
|
|
44
|
+
const path = options.contractPath ?? (options.taskId ? taskArtifactPath(options.taskId, "contract") : ARTIFACT_PATHS.contract);
|
|
45
|
+
errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_CONTRACT_MISSING" : "E_CONTRACT_INVALID", error.message, [path]));
|
|
44
46
|
return null;
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
export async function loadRoute(target, packageRoot, errors) {
|
|
50
|
+
export async function loadRoute(target, packageRoot, errors, options = {}) {
|
|
49
51
|
try {
|
|
50
|
-
return await readPersistedRoute(target, packageRoot);
|
|
52
|
+
return await readPersistedRoute(target, packageRoot, options);
|
|
51
53
|
} catch (error) {
|
|
54
|
+
const path = options.routePath ?? (options.taskId ? taskArtifactPath(options.taskId, "route") : ARTIFACT_PATHS.route);
|
|
52
55
|
const code = error.code === "ARTIFACT_MISSING"
|
|
53
56
|
? "E_ROUTE_MISSING"
|
|
54
57
|
: ["E_ROUTE_REASON_MISSING", "E_ROUTE_INVALID"].includes(error.code) ? error.code : "E_ROUTE_INVALID";
|
|
55
|
-
errors.push(issue(code, error.message, [
|
|
58
|
+
errors.push(issue(code, error.message, [path]));
|
|
56
59
|
return null;
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -74,22 +77,24 @@ export async function loadSources(target, contract, packageRoot, errors) {
|
|
|
74
77
|
return registry;
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
export async function inspectGates(target, contract, route, packageRoot, errors, config = {}) {
|
|
80
|
+
export async function inspectGates(target, contract, route, packageRoot, errors, config = {}, options = {}) {
|
|
78
81
|
if (!route) return { required: [], satisfied: [], records: {} };
|
|
79
82
|
const guideGates = await requiredGatesForGuides(route.value.guides, packageRoot);
|
|
80
83
|
const required = [...new Set([...guideGates, ...(config.requiredGates ?? [])])].sort();
|
|
81
84
|
const satisfied = [];
|
|
82
85
|
const records = {};
|
|
86
|
+
const taskId = options.taskId ?? null;
|
|
83
87
|
for (const gate of required) {
|
|
84
88
|
let artifact;
|
|
89
|
+
const defaultGateRel = taskId ? taskGatePath(taskId, gate) : `${ARTIFACT_PATHS.gates}/${gate}.json`;
|
|
85
90
|
try {
|
|
86
|
-
artifact = await readGateIfPresent(target, gate, packageRoot);
|
|
91
|
+
artifact = await readGateIfPresent(target, gate, packageRoot, { ...options, taskId });
|
|
87
92
|
} catch (error) {
|
|
88
|
-
errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_GATE_UNVERIFIED" : "E_GATE_INVALID", error.message, [
|
|
93
|
+
errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_GATE_UNVERIFIED" : "E_GATE_INVALID", error.message, [defaultGateRel], { gate }));
|
|
89
94
|
continue;
|
|
90
95
|
}
|
|
91
96
|
if (!artifact) {
|
|
92
|
-
errors.push(issue("E_GATE_UNVERIFIED", `Required gate is missing or unverified: ${gate}`, [
|
|
97
|
+
errors.push(issue("E_GATE_UNVERIFIED", `Required gate is missing or unverified: ${gate}`, [defaultGateRel], { gate }));
|
|
93
98
|
continue;
|
|
94
99
|
}
|
|
95
100
|
records[gate] = artifact;
|
package/src/core/preflight.js
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
issue,
|
|
9
9
|
preflightError,
|
|
10
10
|
sortIssues,
|
|
11
|
-
validatePersistedPreflight,
|
|
12
11
|
} from "./preflight-model.js";
|
|
13
12
|
import {
|
|
14
13
|
loadContract,
|
|
@@ -28,31 +27,36 @@ import {
|
|
|
28
27
|
import { PROFILE_PATH } from "./target-layout.js";
|
|
29
28
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
30
29
|
import { validateEventLedger } from "./events.js";
|
|
30
|
+
import { taskArtifactPath } from "./task-paths.js";
|
|
31
31
|
|
|
32
32
|
const PREVIEW_DECISION_LIMIT = 10;
|
|
33
33
|
const PREVIEW_DECISION_MAX_LENGTH = 240;
|
|
34
34
|
|
|
35
35
|
export { validatePersistedPreflight } from "./preflight-model.js";
|
|
36
36
|
|
|
37
|
-
export async function evaluatePreflight({ target, packageRoot, strict = false } = {}) {
|
|
37
|
+
export async function evaluatePreflight({ target, packageRoot, strict = false, taskId = null, contractPath = null, routePath = null, statePath = null } = {}) {
|
|
38
38
|
const errors = [];
|
|
39
39
|
const profile = await readProfile(target);
|
|
40
40
|
const profileProvenance = await validateProfileSources(target, packageRoot);
|
|
41
41
|
errors.push(...(profileProvenance.errors ?? []));
|
|
42
|
-
const contract = await loadContract(target, packageRoot, errors);
|
|
43
|
-
const route = await loadRoute(target, packageRoot, errors);
|
|
42
|
+
const contract = await loadContract(target, packageRoot, errors, { taskId, contractPath });
|
|
43
|
+
const route = await loadRoute(target, packageRoot, errors, { taskId, routePath });
|
|
44
44
|
const config = await optionalConfig(target, packageRoot, errors);
|
|
45
45
|
const effectiveStrict = strict || config.complianceMode === "strict";
|
|
46
46
|
if (effectiveStrict && profile.status !== "verified") {
|
|
47
47
|
errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", [PROFILE_PATH]));
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const contractRelPath = contractPath ?? (taskId ? taskArtifactPath(taskId, "contract") : ARTIFACT_PATHS.contract);
|
|
51
|
+
const routeRelPath = routePath ?? (taskId ? taskArtifactPath(taskId, "route") : ARTIFACT_PATHS.route);
|
|
52
|
+
const stateRelPath = statePath ?? (taskId ? taskArtifactPath(taskId, "state") : ARTIFACT_PATHS.state);
|
|
53
|
+
|
|
50
54
|
const unresolvedDecisions = contract?.value?.unresolvedDecisions ?? [];
|
|
51
55
|
if (unresolvedDecisions.length > 0) {
|
|
52
56
|
errors.push(issue(
|
|
53
57
|
"E_CONTRACT_UNRESOLVED_DECISION",
|
|
54
58
|
"The current contract contains unresolved blocking decisions.",
|
|
55
|
-
[
|
|
59
|
+
[contractRelPath],
|
|
56
60
|
{
|
|
57
61
|
decisions: unresolvedDecisions
|
|
58
62
|
.slice(0, PREVIEW_DECISION_LIMIT)
|
|
@@ -67,32 +71,32 @@ export async function evaluatePreflight({ target, packageRoot, strict = false }
|
|
|
67
71
|
if (route && contract) {
|
|
68
72
|
assertRouteInvariants(route.value);
|
|
69
73
|
if (route.value.contractFingerprint !== undefined && route.value.contractFingerprint !== contract.fingerprint) {
|
|
70
|
-
errors.push(issue("E_ROUTE_STALE", "Routing result was created for a different contract", [
|
|
74
|
+
errors.push(issue("E_ROUTE_STALE", "Routing result was created for a different contract", [routeRelPath, contractRelPath]));
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
const sources = await loadSources(target, contract, packageRoot, errors);
|
|
75
|
-
const gates = await inspectGates(target, contract, route, packageRoot, errors, config);
|
|
79
|
+
const gates = await inspectGates(target, contract, route, packageRoot, errors, config, { taskId });
|
|
76
80
|
|
|
77
81
|
let state = null;
|
|
78
82
|
try {
|
|
79
|
-
state = await readWorkState(target, packageRoot);
|
|
83
|
+
state = await readWorkState(target, { packageRoot, taskId, statePath });
|
|
80
84
|
} catch (error) {
|
|
81
|
-
errors.push(issue("E_STATE_INVALID", error.message, [
|
|
85
|
+
errors.push(issue("E_STATE_INVALID", error.message, [stateRelPath]));
|
|
82
86
|
}
|
|
83
87
|
if (state && route && JSON.stringify(state.selectedGuides) !== JSON.stringify(route.value.guides)) {
|
|
84
|
-
errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "work-state.selectedGuides must equal routing-result.guides", [
|
|
88
|
+
errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "work-state.selectedGuides must equal routing-result.guides", [routeRelPath, stateRelPath]));
|
|
85
89
|
}
|
|
86
90
|
if (state && contract && state.contractFingerprint !== contract.fingerprint) {
|
|
87
|
-
errors.push(issue("E_CONTRACT_STALE", "work-state references a different contract", [
|
|
91
|
+
errors.push(issue("E_CONTRACT_STALE", "work-state references a different contract", [contractRelPath, stateRelPath]));
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
const sortedErrors = sortIssues(errors);
|
|
91
|
-
const
|
|
95
|
+
const effectiveTaskId = taskId ?? contract?.value?.taskId ?? state?.taskId ?? "unknown";
|
|
92
96
|
return {
|
|
93
97
|
schemaVersion: 1,
|
|
94
98
|
protocolVersion: 1,
|
|
95
|
-
taskId,
|
|
99
|
+
taskId: effectiveTaskId,
|
|
96
100
|
status: sortedErrors.length === 0 ? "READY" : "BLOCKED",
|
|
97
101
|
profile: { ...profile, provenance: profileProvenance.status },
|
|
98
102
|
contract: contract
|
|
@@ -128,20 +132,31 @@ export async function validateReadyProtocolConsistency(options = {}) {
|
|
|
128
132
|
});
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
export async function runPreflight({
|
|
132
|
-
|
|
135
|
+
export async function runPreflight({
|
|
136
|
+
target,
|
|
137
|
+
packageRoot,
|
|
138
|
+
strict = false,
|
|
139
|
+
persist = true,
|
|
140
|
+
taskId = null,
|
|
141
|
+
contractPath = null,
|
|
142
|
+
routePath = null,
|
|
143
|
+
statePath = null,
|
|
144
|
+
preflightPath = null,
|
|
145
|
+
eventsPath = null,
|
|
146
|
+
} = {}) {
|
|
147
|
+
let result = await evaluatePreflight({ target, packageRoot, strict, taskId, contractPath, routePath, statePath });
|
|
133
148
|
if (!persist) return result;
|
|
134
149
|
|
|
135
|
-
let ledger = await assertPreflightPersistenceSafety(target, packageRoot,
|
|
150
|
+
let ledger = await assertPreflightPersistenceSafety(target, packageRoot, { taskId, eventsPath });
|
|
136
151
|
assertPreflightResultPersistenceSafety(result);
|
|
137
152
|
assertExistingReadyLifecycleCompatibility(ledger, result);
|
|
138
153
|
|
|
139
154
|
let contract = null;
|
|
140
155
|
let route = null;
|
|
141
156
|
if (result.contract.status === "valid" && result.routing.status === "valid") {
|
|
142
|
-
contract = await readContract(target, packageRoot);
|
|
143
|
-
route = await readPersistedRoute(target, packageRoot);
|
|
144
|
-
const state = await ensureResumableState({ target, packageRoot, contract, route });
|
|
157
|
+
contract = await readContract(target, packageRoot, { taskId, contractPath });
|
|
158
|
+
route = await readPersistedRoute(target, packageRoot, { taskId, routePath });
|
|
159
|
+
const state = await ensureResumableState({ target, packageRoot, contract, route, taskId, statePath });
|
|
145
160
|
if (state) {
|
|
146
161
|
await synchronizePreflightState({
|
|
147
162
|
target,
|
|
@@ -152,22 +167,26 @@ export async function runPreflight({ target, packageRoot, strict = false, persis
|
|
|
152
167
|
requiredGates: result.requiredGates,
|
|
153
168
|
satisfiedGates: result.satisfiedGates,
|
|
154
169
|
complianceMode: result.policy?.complianceMode,
|
|
170
|
+
statePath,
|
|
171
|
+
taskId,
|
|
155
172
|
});
|
|
156
|
-
result = await evaluatePreflight({ target, packageRoot, strict });
|
|
173
|
+
result = await evaluatePreflight({ target, packageRoot, strict, taskId, contractPath, routePath, statePath });
|
|
157
174
|
assertPreflightResultPersistenceSafety(result);
|
|
158
175
|
assertExistingReadyLifecycleCompatibility(ledger, result);
|
|
159
|
-
ledger = await assertPreflightPersistenceSafety(target, packageRoot,
|
|
176
|
+
ledger = await assertPreflightPersistenceSafety(target, packageRoot, { taskId, eventsPath });
|
|
160
177
|
}
|
|
161
178
|
}
|
|
162
179
|
|
|
163
180
|
if (result.taskId !== "unknown") {
|
|
164
|
-
await appendActivationEvents(target, packageRoot, ledger, result);
|
|
165
|
-
const afterEvents = await validateEventLedger(target, packageRoot);
|
|
181
|
+
await appendActivationEvents(target, packageRoot, ledger, result, { eventsPath, taskId });
|
|
182
|
+
const afterEvents = await validateEventLedger(target, packageRoot, { eventsPath, taskId });
|
|
166
183
|
if (!afterEvents.valid) {
|
|
167
184
|
const first = afterEvents.errors[0];
|
|
168
|
-
|
|
185
|
+
const evRel = eventsPath ?? (taskId ? taskArtifactPath(taskId, "events") : ARTIFACT_PATHS.events);
|
|
186
|
+
throw preflightError(first.code, first.message, [evRel]);
|
|
169
187
|
}
|
|
170
188
|
}
|
|
171
|
-
|
|
189
|
+
const relPath = preflightPath ?? (taskId ? taskArtifactPath(taskId, "preflight") : ARTIFACT_PATHS.preflight);
|
|
190
|
+
await writeJsonArtifact(target, relPath, result, "preflight", packageRoot);
|
|
172
191
|
return result;
|
|
173
192
|
}
|
package/src/core/protocol.js
CHANGED
|
@@ -111,17 +111,8 @@ export const WORK_PHASES = Object.freeze([
|
|
|
111
111
|
"BLOCKED",
|
|
112
112
|
]);
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"clean",
|
|
117
|
-
"test",
|
|
118
|
-
"security",
|
|
119
|
-
"design",
|
|
120
|
-
"performance",
|
|
121
|
-
"accessibility",
|
|
122
|
-
"games",
|
|
123
|
-
"taste",
|
|
124
|
-
]);
|
|
114
|
+
import { GUIDE_IDS } from "./guide-registry.js";
|
|
115
|
+
export { GUIDE_IDS };
|
|
125
116
|
|
|
126
117
|
export const GUIDE_ORDER = GUIDE_IDS;
|
|
127
118
|
|
package/src/core/receipt.js
CHANGED
|
@@ -2,7 +2,7 @@ import { GUIDE_IDS, PROTOCOL_VERSION } from "./protocol.js";
|
|
|
2
2
|
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
3
3
|
import { assertEvidenceList, evidenceMatches } from "./evidence.js";
|
|
4
4
|
import { assertJsonLimits } from "./json-safety.js";
|
|
5
|
-
import { assertCheck
|
|
5
|
+
import { assertCheck } from "./checks.js";
|
|
6
6
|
import { assertCoverageList } from "./coverage.js";
|
|
7
7
|
|
|
8
8
|
const RECEIPT_SCHEMA_VERSION = 1;
|
package/src/core/report.js
CHANGED
|
@@ -17,8 +17,8 @@ function section(id, label, status, details = null) {
|
|
|
17
17
|
return { id, label, status, ...(details ? { details } : {}) };
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export async function evaluateReport({ target, packageRoot, strict = false } = {}) {
|
|
21
|
-
const audit = await evaluateAudit({ target, packageRoot, strict });
|
|
20
|
+
export async function evaluateReport({ target, packageRoot, strict = false, ...options } = {}) {
|
|
21
|
+
const audit = await evaluateAudit({ target, packageRoot, strict, ...options });
|
|
22
22
|
const completion = audit.completion;
|
|
23
23
|
const preflight = completion.preflight;
|
|
24
24
|
const errorCodes = new Set(audit.errors.map((error) => error.code));
|