@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
|
@@ -18,9 +18,13 @@ import { readPersistedRoute } from "./route-artifact.js";
|
|
|
18
18
|
import { createReceipt, validateReceipt } from "./receipt.js";
|
|
19
19
|
import { readWorkState, writeWorkState } from "./work-state.js";
|
|
20
20
|
import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
|
|
21
|
-
import { normalizeRequirements
|
|
21
|
+
import { normalizeRequirements } from "./evidence-readiness.js";
|
|
22
22
|
import { classifyCommandResolution, validateVerificationAuthority } from "./verification-capability.js";
|
|
23
23
|
import { readExecutionArtifact, validateExecutionBinding } from "./execution.js";
|
|
24
|
+
import { taskArtifactPath, taskExecutionPath } from "./task-paths.js";
|
|
25
|
+
import { readTaskDescriptor } from "./task-descriptor.js";
|
|
26
|
+
import { assertClaimsCoverChangedPaths } from "./task-scope.js";
|
|
27
|
+
import { discoverTasks } from "./task-discovery.js";
|
|
24
28
|
|
|
25
29
|
function artifactError(code, message, artifacts = []) {
|
|
26
30
|
const error = new Error(message);
|
|
@@ -36,15 +40,16 @@ function requiredString(value, label) {
|
|
|
36
40
|
return value;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
async function readCurrentReceipt(target, packageRoot) {
|
|
43
|
+
async function readCurrentReceipt(target, packageRoot, options = {}) {
|
|
44
|
+
const receiptRel = options.receiptPath ?? (options.taskId ? taskArtifactPath(options.taskId, "receipt") : ARTIFACT_PATHS.receipt);
|
|
40
45
|
try {
|
|
41
|
-
return await readJsonArtifact(target,
|
|
46
|
+
return await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
|
|
42
47
|
} catch (error) {
|
|
43
48
|
if (error.code === "ARTIFACT_MISSING") {
|
|
44
49
|
throw artifactError(
|
|
45
50
|
"E_RECEIPT_MISSING",
|
|
46
51
|
"Execution receipt is missing; run forgeloop prepare-completion first",
|
|
47
|
-
[
|
|
52
|
+
[receiptRel],
|
|
48
53
|
);
|
|
49
54
|
}
|
|
50
55
|
throw error;
|
|
@@ -92,12 +97,13 @@ export async function validateCheckExecutionProvenance(check, {
|
|
|
92
97
|
target,
|
|
93
98
|
executionRef: check.executionRef,
|
|
94
99
|
packageRoot,
|
|
100
|
+
taskId,
|
|
95
101
|
});
|
|
96
102
|
if (!artifact.value) {
|
|
97
103
|
throw artifactError(
|
|
98
104
|
"E_EXECUTION_REF_INVALID",
|
|
99
105
|
"Execution reference does not resolve to an execution artifact in this bundle",
|
|
100
|
-
[ARTIFACT_PATHS.executionDirectory],
|
|
106
|
+
[taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
|
|
101
107
|
);
|
|
102
108
|
}
|
|
103
109
|
const execution = validateExecutionBinding({
|
|
@@ -111,52 +117,51 @@ export async function validateCheckExecutionProvenance(check, {
|
|
|
111
117
|
throw artifactError(
|
|
112
118
|
"E_EXECUTION_REF_INVALID",
|
|
113
119
|
"Execution artifact cwd does not match the current target",
|
|
114
|
-
[ARTIFACT_PATHS.executionDirectory],
|
|
120
|
+
[taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
|
|
115
121
|
);
|
|
116
122
|
}
|
|
117
123
|
if (check.status === "passed" && (execution.status !== "passed" || execution.exitCode !== 0)) {
|
|
118
124
|
throw artifactError(
|
|
119
125
|
"E_EXECUTION_REF_INVALID",
|
|
120
126
|
"A passed command check must reference a successful execution artifact",
|
|
121
|
-
[ARTIFACT_PATHS.executionDirectory],
|
|
127
|
+
[taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
|
|
122
128
|
);
|
|
123
129
|
}
|
|
124
130
|
if (check.status === "failed" && execution.status !== "failed") {
|
|
125
131
|
throw artifactError(
|
|
126
132
|
"E_EXECUTION_REF_INVALID",
|
|
127
133
|
"A failed command check must reference a failed execution artifact",
|
|
128
|
-
[ARTIFACT_PATHS.executionDirectory],
|
|
134
|
+
[taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
|
|
129
135
|
);
|
|
130
136
|
}
|
|
131
137
|
if (check.exitCode !== undefined && check.exitCode !== execution.exitCode) {
|
|
132
138
|
throw artifactError(
|
|
133
139
|
"E_EXECUTION_REF_INVALID",
|
|
134
140
|
"Check exitCode does not match its execution artifact",
|
|
135
|
-
[ARTIFACT_PATHS.executionDirectory],
|
|
141
|
+
[taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
|
|
136
142
|
);
|
|
137
143
|
}
|
|
138
144
|
return execution;
|
|
139
145
|
}
|
|
140
146
|
|
|
141
|
-
export async function validateChecksExecutionProvenance(checks, options
|
|
142
|
-
const
|
|
143
|
-
for (const check of
|
|
144
|
-
|
|
145
|
-
await validateCheckExecutionProvenance(check, options);
|
|
146
|
-
|
|
147
|
-
errors.push({
|
|
148
|
-
code: error.code ?? "E_EXECUTION_REF_INVALID",
|
|
149
|
-
message: error.message,
|
|
150
|
-
artifacts: error.artifacts ?? [options.artifactPath ?? ARTIFACT_PATHS.receipt],
|
|
151
|
-
checkId: check?.id,
|
|
152
|
-
requirementId: check?.requirement,
|
|
153
|
-
});
|
|
147
|
+
export async function validateChecksExecutionProvenance(checks, options) {
|
|
148
|
+
const executions = [];
|
|
149
|
+
for (const check of checks ?? []) {
|
|
150
|
+
if (check.kind === "command" && check.evidenceKind === "OBSERVED") {
|
|
151
|
+
const execution = await validateCheckExecutionProvenance(check, options);
|
|
152
|
+
if (execution) executions.push(execution);
|
|
154
153
|
}
|
|
155
154
|
}
|
|
156
|
-
return
|
|
155
|
+
return executions;
|
|
157
156
|
}
|
|
158
157
|
|
|
159
|
-
export async function requiredEvidenceForTarget({
|
|
158
|
+
export async function requiredEvidenceForTarget({
|
|
159
|
+
target,
|
|
160
|
+
contract,
|
|
161
|
+
route,
|
|
162
|
+
packageRoot,
|
|
163
|
+
additionalEvidence = [],
|
|
164
|
+
}) {
|
|
160
165
|
const config = await readOptionalConfig(target, packageRoot);
|
|
161
166
|
const guideEvidence = await completionEvidenceForGuides(route.value.guides, packageRoot);
|
|
162
167
|
return [...new Set([
|
|
@@ -167,23 +172,36 @@ export async function requiredEvidenceForTarget({ target, contract, route, packa
|
|
|
167
172
|
])].sort();
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
export async function prepareCompletion({
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
175
|
+
export async function prepareCompletion({
|
|
176
|
+
target,
|
|
177
|
+
packageRoot,
|
|
178
|
+
authorityContext,
|
|
179
|
+
runtimeContext,
|
|
180
|
+
taskId = null,
|
|
181
|
+
contractPath = null,
|
|
182
|
+
routePath = null,
|
|
183
|
+
statePath = null,
|
|
184
|
+
receiptPath = null,
|
|
185
|
+
}) {
|
|
186
|
+
const contract = await readContract(target, packageRoot, { taskId, contractPath });
|
|
187
|
+
const route = await readPersistedRoute(target, packageRoot, { taskId, routePath });
|
|
188
|
+
const state = await readWorkState(target, { packageRoot, taskId, statePath });
|
|
189
|
+
const stateRel = statePath ?? (taskId ? taskArtifactPath(taskId, "state") : ARTIFACT_PATHS.state);
|
|
190
|
+
const receiptRel = receiptPath ?? (taskId ? taskArtifactPath(taskId, "receipt") : ARTIFACT_PATHS.receipt);
|
|
191
|
+
|
|
174
192
|
if (!state) {
|
|
175
|
-
throw artifactError("E_STATE_MISSING", "Work state is required before preparing completion", [
|
|
193
|
+
throw artifactError("E_STATE_MISSING", "Work state is required before preparing completion", [stateRel]);
|
|
176
194
|
}
|
|
177
195
|
if (hasExecutionStarted(state.phase)) {
|
|
178
|
-
await assertExecutionPrerequisites({ target, state, packageRoot });
|
|
196
|
+
await assertExecutionPrerequisites({ target, state, packageRoot, taskId, statePath, contractPath, routePath });
|
|
179
197
|
}
|
|
180
198
|
|
|
181
199
|
let existing = null;
|
|
182
200
|
try {
|
|
183
|
-
existing = await readJsonArtifact(target,
|
|
201
|
+
existing = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
|
|
184
202
|
await validateReceipt(existing.value, packageRoot, {
|
|
185
203
|
target,
|
|
186
|
-
taskId: contract
|
|
204
|
+
taskId: contract.value.taskId,
|
|
187
205
|
authorityContext,
|
|
188
206
|
runtimeContext,
|
|
189
207
|
});
|
|
@@ -191,7 +209,7 @@ export async function prepareCompletion({ target, packageRoot, authorityContext,
|
|
|
191
209
|
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
192
210
|
}
|
|
193
211
|
|
|
194
|
-
const preflight = await evaluatePreflight({ target, packageRoot });
|
|
212
|
+
const preflight = await evaluatePreflight({ target, packageRoot, taskId, contractPath, routePath, statePath });
|
|
195
213
|
const requiredEvidence = await requiredEvidenceForTarget({
|
|
196
214
|
target,
|
|
197
215
|
contract,
|
|
@@ -201,13 +219,46 @@ export async function prepareCompletion({ target, packageRoot, authorityContext,
|
|
|
201
219
|
});
|
|
202
220
|
const existingValue = existing?.value ?? {};
|
|
203
221
|
if (existingValue.taskId && existingValue.taskId !== contract.value.taskId) {
|
|
204
|
-
throw artifactError("E_RECEIPT_TASK_MISMATCH", "Execution receipt does not belong to the current contract task", [
|
|
222
|
+
throw artifactError("E_RECEIPT_TASK_MISMATCH", "Execution receipt does not belong to the current contract task", [receiptRel]);
|
|
205
223
|
}
|
|
206
224
|
if (existing && existingValue.stateFingerprint === undefined) {
|
|
207
|
-
throw artifactError("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [
|
|
225
|
+
throw artifactError("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [receiptRel]);
|
|
208
226
|
}
|
|
209
227
|
assertStateIdentity({ contract, route, state });
|
|
210
|
-
|
|
228
|
+
|
|
229
|
+
let writeClaims = [];
|
|
230
|
+
try {
|
|
231
|
+
const desc = taskId ? await readTaskDescriptor(target, taskId, packageRoot) : null;
|
|
232
|
+
writeClaims = desc?.value?.writeClaims ?? [];
|
|
233
|
+
} catch {
|
|
234
|
+
// descriptor may not exist
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let observedPaths = null;
|
|
238
|
+
if (writeClaims.length > 0) {
|
|
239
|
+
observedPaths = await currentChangedPaths(target, { paths: writeClaims });
|
|
240
|
+
if (observedPaths !== null) {
|
|
241
|
+
assertClaimsCoverChangedPaths(writeClaims, observedPaths);
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
observedPaths = await currentChangedPaths(target);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (observedPaths === null) {
|
|
248
|
+
const allTasks = await discoverTasks(target, packageRoot);
|
|
249
|
+
const nonComplete = allTasks.filter((t) => t.phase !== "COMPLETE");
|
|
250
|
+
if (nonComplete.length > 1) {
|
|
251
|
+
const mode = preflight.policy?.complianceMode ?? "standard";
|
|
252
|
+
if (mode === "standard" || mode === "strict") {
|
|
253
|
+
throw artifactError(
|
|
254
|
+
"E_TASK_CHANGE_ATTRIBUTION_UNAVAILABLE",
|
|
255
|
+
"Cannot attribute changed files across multiple concurrent tasks in a non-Git target",
|
|
256
|
+
[receiptRel],
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
211
262
|
const changedPaths = observedPaths !== null
|
|
212
263
|
? [...observedPaths]
|
|
213
264
|
: existing
|
|
@@ -264,7 +315,7 @@ export async function prepareCompletion({ target, packageRoot, authorityContext,
|
|
|
264
315
|
});
|
|
265
316
|
const written = await writeJsonArtifact(
|
|
266
317
|
target,
|
|
267
|
-
|
|
318
|
+
receiptRel,
|
|
268
319
|
receipt,
|
|
269
320
|
"execution-receipt",
|
|
270
321
|
packageRoot,
|
|
@@ -306,24 +357,33 @@ export async function assertRecordCheckPrerequisites({
|
|
|
306
357
|
evidenceKind,
|
|
307
358
|
authorityContext,
|
|
308
359
|
runtimeContext,
|
|
360
|
+
taskId = null,
|
|
361
|
+
contractPath = null,
|
|
362
|
+
routePath = null,
|
|
363
|
+
statePath = null,
|
|
364
|
+
receiptPath = null,
|
|
365
|
+
eventsPath = null,
|
|
309
366
|
} = {}) {
|
|
310
|
-
const state = await readWorkState(target, packageRoot);
|
|
311
|
-
|
|
367
|
+
const state = await readWorkState(target, { packageRoot, taskId, statePath });
|
|
368
|
+
const stateRel = statePath ?? (taskId ? taskArtifactPath(taskId, "state") : ARTIFACT_PATHS.state);
|
|
369
|
+
const eventsRel = eventsPath ?? (taskId ? taskArtifactPath(taskId, "events") : ARTIFACT_PATHS.events);
|
|
370
|
+
|
|
371
|
+
if (!state) throw artifactError("E_STATE_MISSING", "Work state is required before recording a check", [stateRel]);
|
|
312
372
|
if (["COMPLETE", "BLOCKED"].includes(state.phase)) {
|
|
313
|
-
throw artifactError("E_PHASE_TRANSITION_INVALID", `Cannot record a check in ${state.phase}`, [
|
|
373
|
+
throw artifactError("E_PHASE_TRANSITION_INVALID", `Cannot record a check in ${state.phase}`, [stateRel]);
|
|
314
374
|
}
|
|
315
375
|
if (state.phase !== "VERIFYING") {
|
|
316
376
|
throw artifactError(
|
|
317
377
|
"E_PHASE_PREREQUISITE_MISSING",
|
|
318
378
|
`record-check requires VERIFYING before review; found ${state.phase}`,
|
|
319
|
-
[
|
|
379
|
+
[stateRel],
|
|
320
380
|
);
|
|
321
381
|
}
|
|
322
|
-
await assertExecutionPrerequisites({ target, state, packageRoot });
|
|
382
|
+
await assertExecutionPrerequisites({ target, state, packageRoot, taskId, statePath, contractPath, routePath });
|
|
323
383
|
|
|
324
|
-
const contract = await readContract(target, packageRoot);
|
|
325
|
-
const route = await readPersistedRoute(target, packageRoot);
|
|
326
|
-
const preflight = await evaluatePreflight({ target, packageRoot });
|
|
384
|
+
const contract = await readContract(target, packageRoot, { taskId, contractPath });
|
|
385
|
+
const route = await readPersistedRoute(target, packageRoot, { taskId, routePath });
|
|
386
|
+
const preflight = await evaluatePreflight({ target, packageRoot, taskId, contractPath, routePath, statePath });
|
|
327
387
|
const requiredEvidence = await requiredEvidenceForTarget({
|
|
328
388
|
target,
|
|
329
389
|
contract,
|
|
@@ -338,27 +398,27 @@ export async function assertRecordCheckPrerequisites({
|
|
|
338
398
|
throw artifactError(
|
|
339
399
|
"E_FUTURE_LIFECYCLE_EVIDENCE",
|
|
340
400
|
`Terminal-owned requirement cannot be recorded before its authoritative result: ${requested.text}`,
|
|
341
|
-
[
|
|
401
|
+
[stateRel, eventsRel],
|
|
342
402
|
);
|
|
343
403
|
}
|
|
344
404
|
|
|
345
|
-
const existingReceipt = await readCurrentReceipt(target, packageRoot);
|
|
405
|
+
const existingReceipt = await readCurrentReceipt(target, packageRoot, { taskId, receiptPath });
|
|
346
406
|
await validateReceipt(existingReceipt.value, packageRoot, {
|
|
347
407
|
target,
|
|
348
408
|
taskId: contract.value.taskId,
|
|
349
409
|
authorityContext,
|
|
350
410
|
runtimeContext,
|
|
351
411
|
});
|
|
352
|
-
const ledger = await validateEventLedger(target, packageRoot);
|
|
412
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId, eventsPath });
|
|
353
413
|
if (!ledger.valid) {
|
|
354
414
|
const first = ledger.errors[0];
|
|
355
|
-
throw artifactError(first.code, first.message, [
|
|
415
|
+
throw artifactError(first.code, first.message, [eventsRel]);
|
|
356
416
|
}
|
|
357
417
|
if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
|
|
358
418
|
throw artifactError(
|
|
359
419
|
"E_PHASE_CHRONOLOGY_INVALID",
|
|
360
420
|
"record-check requires VERIFICATION_STARTED in the current task ledger",
|
|
361
|
-
[
|
|
421
|
+
[eventsRel],
|
|
362
422
|
);
|
|
363
423
|
}
|
|
364
424
|
return {
|
|
@@ -388,6 +448,12 @@ export async function recordCheck({
|
|
|
388
448
|
provenance,
|
|
389
449
|
authorityContext,
|
|
390
450
|
runtimeContext,
|
|
451
|
+
taskId = null,
|
|
452
|
+
contractPath = null,
|
|
453
|
+
routePath = null,
|
|
454
|
+
statePath = null,
|
|
455
|
+
receiptPath = null,
|
|
456
|
+
eventsPath = null,
|
|
391
457
|
}) {
|
|
392
458
|
requiredString(id, "check id");
|
|
393
459
|
requiredString(kind, "check kind");
|
|
@@ -422,6 +488,12 @@ export async function recordCheck({
|
|
|
422
488
|
evidenceKind,
|
|
423
489
|
authorityContext,
|
|
424
490
|
runtimeContext,
|
|
491
|
+
taskId,
|
|
492
|
+
contractPath,
|
|
493
|
+
routePath,
|
|
494
|
+
statePath,
|
|
495
|
+
receiptPath,
|
|
496
|
+
eventsPath,
|
|
425
497
|
});
|
|
426
498
|
const {
|
|
427
499
|
state,
|
|
@@ -431,6 +503,8 @@ export async function recordCheck({
|
|
|
431
503
|
existingReceipt,
|
|
432
504
|
} = context;
|
|
433
505
|
|
|
506
|
+
const receiptRel = receiptPath ?? (taskId ? taskArtifactPath(taskId, "receipt") : ARTIFACT_PATHS.receipt);
|
|
507
|
+
|
|
434
508
|
const commandSpec = typeof command === "string" && command.trim() !== "" ? command.trim() : undefined;
|
|
435
509
|
const observedCommand = kind === "command" && evidenceKind === "OBSERVED";
|
|
436
510
|
if (observedCommand && (!executionRef || provenance !== "FORGELOOP_EXECUTED")) {
|
|
@@ -525,7 +599,7 @@ export async function recordCheck({
|
|
|
525
599
|
runtimeContext,
|
|
526
600
|
});
|
|
527
601
|
if (!auth.valid) {
|
|
528
|
-
throw artifactError(auth.error.code, auth.error.message, [
|
|
602
|
+
throw artifactError(auth.error.code, auth.error.message, [receiptRel]);
|
|
529
603
|
}
|
|
530
604
|
}
|
|
531
605
|
|
|
@@ -582,10 +656,10 @@ export async function recordCheck({
|
|
|
582
656
|
runtimeContext,
|
|
583
657
|
});
|
|
584
658
|
|
|
585
|
-
await writeWorkState(target, nextState, { packageRoot });
|
|
659
|
+
await writeWorkState(target, nextState, { packageRoot, taskId, statePath });
|
|
586
660
|
const written = await writeJsonArtifact(
|
|
587
661
|
target,
|
|
588
|
-
|
|
662
|
+
receiptRel,
|
|
589
663
|
nextReceipt,
|
|
590
664
|
"execution-receipt",
|
|
591
665
|
packageRoot,
|
|
@@ -600,7 +674,7 @@ export async function recordCheck({
|
|
|
600
674
|
evidenceKind: check.evidenceKind,
|
|
601
675
|
verificationCycle: state.verificationCycle ?? 1,
|
|
602
676
|
},
|
|
603
|
-
}, packageRoot);
|
|
677
|
+
}, packageRoot, { taskId, eventsPath });
|
|
604
678
|
return {
|
|
605
679
|
path: written.path,
|
|
606
680
|
receipt: written.value,
|
|
@@ -622,6 +696,12 @@ export async function recordTerminalResult({
|
|
|
622
696
|
details = {},
|
|
623
697
|
authorityContext,
|
|
624
698
|
runtimeContext,
|
|
699
|
+
taskId = null,
|
|
700
|
+
contractPath = null,
|
|
701
|
+
routePath = null,
|
|
702
|
+
statePath = null,
|
|
703
|
+
receiptPath = null,
|
|
704
|
+
eventsPath = null,
|
|
625
705
|
} = {}) {
|
|
626
706
|
if (!target || !requirement || !type || !status || !source || !result) {
|
|
627
707
|
throw artifactError("E_CHECK_INVALID", "record-terminal-result requires target, requirement, type, status, source, and result", [ARTIFACT_PATHS.state]);
|
|
@@ -636,16 +716,19 @@ export async function recordTerminalResult({
|
|
|
636
716
|
throw artifactError("E_CHECK_INVALID", `Invalid production readiness status for record-terminal-result: ${status}`, [ARTIFACT_PATHS.state]);
|
|
637
717
|
}
|
|
638
718
|
|
|
639
|
-
const state = await readWorkState(target, packageRoot);
|
|
640
|
-
|
|
719
|
+
const state = await readWorkState(target, { packageRoot, taskId, statePath });
|
|
720
|
+
const stateRel = statePath ?? (taskId ? taskArtifactPath(taskId, "state") : ARTIFACT_PATHS.state);
|
|
721
|
+
const receiptRel = receiptPath ?? (taskId ? taskArtifactPath(taskId, "receipt") : ARTIFACT_PATHS.receipt);
|
|
722
|
+
|
|
723
|
+
if (!state) throw artifactError("E_STATE_MISSING", "Work state is required before recording a terminal result", [stateRel]);
|
|
641
724
|
if (["COMPLETE", "BLOCKED"].includes(state.phase)) {
|
|
642
|
-
throw artifactError("E_PHASE_TRANSITION_INVALID", `Cannot record a terminal result in ${state.phase}`, [
|
|
725
|
+
throw artifactError("E_PHASE_TRANSITION_INVALID", `Cannot record a terminal result in ${state.phase}`, [stateRel]);
|
|
643
726
|
}
|
|
644
|
-
await assertExecutionPrerequisites({ target, state, packageRoot });
|
|
727
|
+
await assertExecutionPrerequisites({ target, state, packageRoot, taskId, statePath, contractPath, routePath });
|
|
645
728
|
|
|
646
|
-
const contract = await readContract(target, packageRoot);
|
|
647
|
-
const route = await readPersistedRoute(target, packageRoot);
|
|
648
|
-
const preflight = await evaluatePreflight({ target, packageRoot });
|
|
729
|
+
const contract = await readContract(target, packageRoot, { taskId, contractPath });
|
|
730
|
+
const route = await readPersistedRoute(target, packageRoot, { taskId, routePath });
|
|
731
|
+
const preflight = await evaluatePreflight({ target, packageRoot, taskId, contractPath, routePath, statePath });
|
|
649
732
|
const requiredEvidence = await requiredEvidenceForTarget({
|
|
650
733
|
target,
|
|
651
734
|
contract,
|
|
@@ -663,7 +746,7 @@ export async function recordTerminalResult({
|
|
|
663
746
|
throw artifactError(
|
|
664
747
|
"E_TERMINAL_REQUIREMENT_UNKNOWN",
|
|
665
748
|
`Terminal results must reference an existing canonical terminal requirement: ${requirement}`,
|
|
666
|
-
[
|
|
749
|
+
[contractRelPath(contract, taskId), receiptRel],
|
|
667
750
|
);
|
|
668
751
|
}
|
|
669
752
|
|
|
@@ -671,7 +754,7 @@ export async function recordTerminalResult({
|
|
|
671
754
|
throw artifactError(
|
|
672
755
|
"E_TERMINAL_REQUIREMENT_NOT_TERMINAL",
|
|
673
756
|
`record-terminal-result may only target terminal-owned requirements; found ${requested.type}: ${requested.text}`,
|
|
674
|
-
[
|
|
757
|
+
[contractRelPath(contract, taskId), receiptRel],
|
|
675
758
|
);
|
|
676
759
|
}
|
|
677
760
|
|
|
@@ -679,11 +762,11 @@ export async function recordTerminalResult({
|
|
|
679
762
|
throw artifactError(
|
|
680
763
|
"E_TERMINAL_REQUIREMENT_TYPE_MISMATCH",
|
|
681
764
|
`Requirement ${requested.id} is ${requested.type}, not ${type}.`,
|
|
682
|
-
[
|
|
765
|
+
[contractRelPath(contract, taskId), receiptRel],
|
|
683
766
|
);
|
|
684
767
|
}
|
|
685
768
|
|
|
686
|
-
const existingReceipt = await readCurrentReceipt(target, packageRoot);
|
|
769
|
+
const existingReceipt = await readCurrentReceipt(target, packageRoot, { taskId, receiptPath });
|
|
687
770
|
await validateReceipt(existingReceipt.value, packageRoot, {
|
|
688
771
|
target,
|
|
689
772
|
taskId: contract?.value?.taskId,
|
|
@@ -704,7 +787,7 @@ export async function recordTerminalResult({
|
|
|
704
787
|
throw artifactError(
|
|
705
788
|
"E_TERMINAL_STATUS_REGRESSION",
|
|
706
789
|
`Publication status cannot regress from ${prev} to ${status}.`,
|
|
707
|
-
[
|
|
790
|
+
[receiptRel],
|
|
708
791
|
);
|
|
709
792
|
}
|
|
710
793
|
}
|
|
@@ -723,7 +806,7 @@ export async function recordTerminalResult({
|
|
|
723
806
|
));
|
|
724
807
|
|
|
725
808
|
if (isIdentical) {
|
|
726
|
-
const ledger = await validateEventLedger(target, packageRoot);
|
|
809
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId, eventsPath });
|
|
727
810
|
const matchingEvent = ledger.events.find((event) => (
|
|
728
811
|
event.taskId === state.taskId
|
|
729
812
|
&& event.event === "TERMINAL_RESULT_RECORDED"
|
|
@@ -737,7 +820,7 @@ export async function recordTerminalResult({
|
|
|
737
820
|
|
|
738
821
|
if (matchingEvent) {
|
|
739
822
|
return {
|
|
740
|
-
path: path.join(target,
|
|
823
|
+
path: path.join(target, receiptRel),
|
|
741
824
|
receipt: existingReceipt.value,
|
|
742
825
|
requirementId: requested.id,
|
|
743
826
|
type,
|
|
@@ -759,10 +842,10 @@ export async function recordTerminalResult({
|
|
|
759
842
|
source: source.trim(),
|
|
760
843
|
result: result.trim(),
|
|
761
844
|
},
|
|
762
|
-
}, packageRoot);
|
|
845
|
+
}, packageRoot, { taskId, eventsPath });
|
|
763
846
|
|
|
764
847
|
return {
|
|
765
|
-
path: path.join(target,
|
|
848
|
+
path: path.join(target, receiptRel),
|
|
766
849
|
receipt: existingReceipt.value,
|
|
767
850
|
requirementId: requested.id,
|
|
768
851
|
type,
|
|
@@ -823,10 +906,10 @@ export async function recordTerminalResult({
|
|
|
823
906
|
runtimeContext,
|
|
824
907
|
});
|
|
825
908
|
|
|
826
|
-
await writeWorkState(target, nextState, { packageRoot });
|
|
909
|
+
await writeWorkState(target, nextState, { packageRoot, taskId, statePath });
|
|
827
910
|
await writeJsonArtifact(
|
|
828
911
|
target,
|
|
829
|
-
|
|
912
|
+
receiptRel,
|
|
830
913
|
nextReceipt,
|
|
831
914
|
"execution-receipt",
|
|
832
915
|
packageRoot,
|
|
@@ -843,10 +926,10 @@ export async function recordTerminalResult({
|
|
|
843
926
|
source: source.trim(),
|
|
844
927
|
result: result.trim(),
|
|
845
928
|
},
|
|
846
|
-
}, packageRoot);
|
|
929
|
+
}, packageRoot, { taskId, eventsPath });
|
|
847
930
|
|
|
848
931
|
return {
|
|
849
|
-
path: path.join(target,
|
|
932
|
+
path: path.join(target, receiptRel),
|
|
850
933
|
receipt: nextReceipt,
|
|
851
934
|
requirementId: requested.id,
|
|
852
935
|
type,
|
|
@@ -855,3 +938,7 @@ export async function recordTerminalResult({
|
|
|
855
938
|
event,
|
|
856
939
|
};
|
|
857
940
|
}
|
|
941
|
+
|
|
942
|
+
function contractRelPath(contract, taskId) {
|
|
943
|
+
return taskId ? taskArtifactPath(taskId, "contract") : ARTIFACT_PATHS.contract;
|
|
944
|
+
}
|