@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
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { readContract } from "./contract.js";
|
|
9
9
|
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
10
10
|
import { completionEvidenceForGuides } from "./guide-metadata.js";
|
|
11
|
-
import { createCheck } from "./checks.js";
|
|
11
|
+
import { CHECK_PROVENANCE, createCheck } from "./checks.js";
|
|
12
12
|
import { createEvidence } from "./evidence.js";
|
|
13
13
|
import { coverageForRequirements } from "./coverage.js";
|
|
14
14
|
import { assertCompletionRelationships, assertStateIdentity } from "./completion-relationships.js";
|
|
@@ -20,6 +20,7 @@ import { readWorkState, writeWorkState } from "./work-state.js";
|
|
|
20
20
|
import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
|
|
21
21
|
import { normalizeRequirements, classifyRequirement } from "./evidence-readiness.js";
|
|
22
22
|
import { classifyCommandResolution, validateVerificationAuthority } from "./verification-capability.js";
|
|
23
|
+
import { readExecutionArtifact, validateExecutionBinding } from "./execution.js";
|
|
23
24
|
|
|
24
25
|
function artifactError(code, message, artifacts = []) {
|
|
25
26
|
const error = new Error(message);
|
|
@@ -59,6 +60,102 @@ async function readOptionalConfig(target, packageRoot) {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
function formatArgv(argv) {
|
|
64
|
+
return argv.map((argument) => /[\s"']/u.test(argument)
|
|
65
|
+
? JSON.stringify(argument)
|
|
66
|
+
: argument).join(" ");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function commandProvenanceError(message = "Observed command evidence requires a ForgeLoop execution artifact") {
|
|
70
|
+
return artifactError("E_COMMAND_PROVENANCE_UNATTESTED", message, [ARTIFACT_PATHS.receipt]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Revalidate the ForgeLoop-owned execution artifact behind an observed command
|
|
75
|
+
* check. This is intentionally asynchronous so completion and audit can verify
|
|
76
|
+
* the artifact instead of trusting duplicated check metadata.
|
|
77
|
+
*/
|
|
78
|
+
export async function validateCheckExecutionProvenance(check, {
|
|
79
|
+
target,
|
|
80
|
+
packageRoot,
|
|
81
|
+
taskId,
|
|
82
|
+
executionArtifacts,
|
|
83
|
+
allowForeignCwd = false,
|
|
84
|
+
} = {}) {
|
|
85
|
+
if (check?.kind !== "command" || check.evidenceKind !== "OBSERVED") return null;
|
|
86
|
+
if (check.provenance !== "FORGELOOP_EXECUTED" || !check.executionRef) {
|
|
87
|
+
throw commandProvenanceError();
|
|
88
|
+
}
|
|
89
|
+
const artifact = executionArtifacts
|
|
90
|
+
? { value: executionArtifacts[check.executionRef] }
|
|
91
|
+
: await readExecutionArtifact({
|
|
92
|
+
target,
|
|
93
|
+
executionRef: check.executionRef,
|
|
94
|
+
packageRoot,
|
|
95
|
+
});
|
|
96
|
+
if (!artifact.value) {
|
|
97
|
+
throw artifactError(
|
|
98
|
+
"E_EXECUTION_REF_INVALID",
|
|
99
|
+
"Execution reference does not resolve to an execution artifact in this bundle",
|
|
100
|
+
[ARTIFACT_PATHS.executionDirectory],
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const execution = validateExecutionBinding({
|
|
104
|
+
execution: artifact.value,
|
|
105
|
+
taskId,
|
|
106
|
+
checkId: check.id,
|
|
107
|
+
requirement: check.requirement,
|
|
108
|
+
verificationCycle: check.details?.verificationCycle ?? 1,
|
|
109
|
+
});
|
|
110
|
+
if (!allowForeignCwd && path.resolve(execution.cwd) !== path.resolve(target)) {
|
|
111
|
+
throw artifactError(
|
|
112
|
+
"E_EXECUTION_REF_INVALID",
|
|
113
|
+
"Execution artifact cwd does not match the current target",
|
|
114
|
+
[ARTIFACT_PATHS.executionDirectory],
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (check.status === "passed" && (execution.status !== "passed" || execution.exitCode !== 0)) {
|
|
118
|
+
throw artifactError(
|
|
119
|
+
"E_EXECUTION_REF_INVALID",
|
|
120
|
+
"A passed command check must reference a successful execution artifact",
|
|
121
|
+
[ARTIFACT_PATHS.executionDirectory],
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (check.status === "failed" && execution.status !== "failed") {
|
|
125
|
+
throw artifactError(
|
|
126
|
+
"E_EXECUTION_REF_INVALID",
|
|
127
|
+
"A failed command check must reference a failed execution artifact",
|
|
128
|
+
[ARTIFACT_PATHS.executionDirectory],
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (check.exitCode !== undefined && check.exitCode !== execution.exitCode) {
|
|
132
|
+
throw artifactError(
|
|
133
|
+
"E_EXECUTION_REF_INVALID",
|
|
134
|
+
"Check exitCode does not match its execution artifact",
|
|
135
|
+
[ARTIFACT_PATHS.executionDirectory],
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return execution;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function validateChecksExecutionProvenance(checks, options = {}) {
|
|
142
|
+
const errors = [];
|
|
143
|
+
for (const check of Array.isArray(checks) ? checks : []) {
|
|
144
|
+
try {
|
|
145
|
+
await validateCheckExecutionProvenance(check, options);
|
|
146
|
+
} catch (error) {
|
|
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
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return errors;
|
|
157
|
+
}
|
|
158
|
+
|
|
62
159
|
export async function requiredEvidenceForTarget({ target, contract, route, packageRoot, additionalEvidence = [] }) {
|
|
63
160
|
const config = await readOptionalConfig(target, packageRoot);
|
|
64
161
|
const guideEvidence = await completionEvidenceForGuides(route.value.guides, packageRoot);
|
|
@@ -196,42 +293,20 @@ function appendUniqueEvidence(evidence, nextEvidence) {
|
|
|
196
293
|
return exists ? [...evidence] : [...evidence, nextEvidence];
|
|
197
294
|
}
|
|
198
295
|
|
|
199
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Read-only lifecycle checks shared by run-check and record-check. Keeping
|
|
298
|
+
* these checks before process launch prevents a command from running when the
|
|
299
|
+
* target is not ready to receive verification evidence.
|
|
300
|
+
*/
|
|
301
|
+
export async function assertRecordCheckPrerequisites({
|
|
200
302
|
target,
|
|
201
303
|
packageRoot,
|
|
202
|
-
id,
|
|
203
|
-
kind = "command",
|
|
204
304
|
requirement,
|
|
205
305
|
status,
|
|
206
306
|
evidenceKind,
|
|
207
|
-
command,
|
|
208
|
-
result,
|
|
209
|
-
exitCode,
|
|
210
|
-
details,
|
|
211
307
|
authorityContext,
|
|
212
308
|
runtimeContext,
|
|
213
|
-
}) {
|
|
214
|
-
requiredString(id, "check id");
|
|
215
|
-
requiredString(kind, "check kind");
|
|
216
|
-
requiredString(requirement, "check requirement");
|
|
217
|
-
requiredString(status, "check status");
|
|
218
|
-
requiredString(evidenceKind, "evidence kind");
|
|
219
|
-
if (command !== undefined && typeof command !== "string") {
|
|
220
|
-
throw artifactError("E_CHECK_INVALID", "command must be a string when supplied");
|
|
221
|
-
}
|
|
222
|
-
if (result !== undefined && typeof result !== "string") {
|
|
223
|
-
throw artifactError("E_CHECK_INVALID", "result must be a string when supplied");
|
|
224
|
-
}
|
|
225
|
-
if (details !== undefined && (!details || typeof details !== "object" || Array.isArray(details))) {
|
|
226
|
-
throw artifactError("E_CHECK_INVALID", "check details must be a JSON object");
|
|
227
|
-
}
|
|
228
|
-
if ((typeof command !== "string" || command.trim() === "")
|
|
229
|
-
&& (typeof result !== "string" || result.trim() === "")) {
|
|
230
|
-
throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const commandSpec = typeof command === "string" && command.trim() !== "" ? command.trim() : undefined;
|
|
234
|
-
|
|
309
|
+
} = {}) {
|
|
235
310
|
const state = await readWorkState(target, packageRoot);
|
|
236
311
|
if (!state) throw artifactError("E_STATE_MISSING", "Work state is required before recording a check", [ARTIFACT_PATHS.state]);
|
|
237
312
|
if (["COMPLETE", "BLOCKED"].includes(state.phase)) {
|
|
@@ -274,9 +349,114 @@ export async function recordCheck({
|
|
|
274
349
|
authorityContext,
|
|
275
350
|
runtimeContext,
|
|
276
351
|
});
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
352
|
+
const ledger = await validateEventLedger(target, packageRoot);
|
|
353
|
+
if (!ledger.valid) {
|
|
354
|
+
const first = ledger.errors[0];
|
|
355
|
+
throw artifactError(first.code, first.message, [ARTIFACT_PATHS.events]);
|
|
356
|
+
}
|
|
357
|
+
if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
|
|
358
|
+
throw artifactError(
|
|
359
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
360
|
+
"record-check requires VERIFICATION_STARTED in the current task ledger",
|
|
361
|
+
[ARTIFACT_PATHS.events],
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
state,
|
|
366
|
+
contract,
|
|
367
|
+
route,
|
|
368
|
+
preflight,
|
|
369
|
+
requiredEvidence,
|
|
370
|
+
existingReceipt,
|
|
371
|
+
ledger,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function recordCheck({
|
|
376
|
+
target,
|
|
377
|
+
packageRoot,
|
|
378
|
+
id,
|
|
379
|
+
kind = "command",
|
|
380
|
+
requirement,
|
|
381
|
+
status,
|
|
382
|
+
evidenceKind,
|
|
383
|
+
command,
|
|
384
|
+
result,
|
|
385
|
+
exitCode,
|
|
386
|
+
details,
|
|
387
|
+
executionRef,
|
|
388
|
+
provenance,
|
|
389
|
+
authorityContext,
|
|
390
|
+
runtimeContext,
|
|
391
|
+
}) {
|
|
392
|
+
requiredString(id, "check id");
|
|
393
|
+
requiredString(kind, "check kind");
|
|
394
|
+
requiredString(requirement, "check requirement");
|
|
395
|
+
requiredString(status, "check status");
|
|
396
|
+
requiredString(evidenceKind, "evidence kind");
|
|
397
|
+
if (command !== undefined && typeof command !== "string") {
|
|
398
|
+
throw artifactError("E_CHECK_INVALID", "command must be a string when supplied");
|
|
399
|
+
}
|
|
400
|
+
if (result !== undefined && typeof result !== "string") {
|
|
401
|
+
throw artifactError("E_CHECK_INVALID", "result must be a string when supplied");
|
|
402
|
+
}
|
|
403
|
+
if (details !== undefined && (!details || typeof details !== "object" || Array.isArray(details))) {
|
|
404
|
+
throw artifactError("E_CHECK_INVALID", "check details must be a JSON object");
|
|
405
|
+
}
|
|
406
|
+
if (executionRef !== undefined && (typeof executionRef !== "string" || executionRef.trim() === "")) {
|
|
407
|
+
throw artifactError("E_EXECUTION_REF_INVALID", "executionRef must be a non-empty string when supplied");
|
|
408
|
+
}
|
|
409
|
+
if (provenance !== undefined && !CHECK_PROVENANCE.includes(provenance)) {
|
|
410
|
+
throw artifactError("E_CHECK_INVALID", `provenance must be one of ${CHECK_PROVENANCE.join(", ")}`);
|
|
411
|
+
}
|
|
412
|
+
if ((typeof command !== "string" || command.trim() === "")
|
|
413
|
+
&& (typeof result !== "string" || result.trim() === "")) {
|
|
414
|
+
throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const context = await assertRecordCheckPrerequisites({
|
|
418
|
+
target,
|
|
419
|
+
packageRoot,
|
|
420
|
+
requirement,
|
|
421
|
+
status,
|
|
422
|
+
evidenceKind,
|
|
423
|
+
authorityContext,
|
|
424
|
+
runtimeContext,
|
|
425
|
+
});
|
|
426
|
+
const {
|
|
427
|
+
state,
|
|
428
|
+
contract,
|
|
429
|
+
route,
|
|
430
|
+
requiredEvidence,
|
|
431
|
+
existingReceipt,
|
|
432
|
+
} = context;
|
|
433
|
+
|
|
434
|
+
const commandSpec = typeof command === "string" && command.trim() !== "" ? command.trim() : undefined;
|
|
435
|
+
const observedCommand = kind === "command" && evidenceKind === "OBSERVED";
|
|
436
|
+
if (observedCommand && (!executionRef || provenance !== "FORGELOOP_EXECUTED")) {
|
|
437
|
+
throw commandProvenanceError();
|
|
438
|
+
}
|
|
439
|
+
const execution = executionRef
|
|
440
|
+
? await validateCheckExecutionProvenance({
|
|
441
|
+
kind,
|
|
442
|
+
evidenceKind,
|
|
443
|
+
provenance,
|
|
444
|
+
executionRef,
|
|
445
|
+
status,
|
|
446
|
+
exitCode,
|
|
447
|
+
id,
|
|
448
|
+
requirement,
|
|
449
|
+
details: { ...(details ?? {}), verificationCycle: state.verificationCycle ?? 1 },
|
|
450
|
+
}, {
|
|
451
|
+
target,
|
|
452
|
+
packageRoot,
|
|
453
|
+
taskId: contract.value.taskId,
|
|
454
|
+
})
|
|
455
|
+
: null;
|
|
456
|
+
const effectiveCommand = execution ? formatArgv(execution.argv) : commandSpec;
|
|
457
|
+
const source = effectiveCommand || `check:${id}`;
|
|
458
|
+
const recordedResult = result?.trim() || `recorded command: ${effectiveCommand || source}`;
|
|
459
|
+
const classification = execution?.resolution ?? (effectiveCommand !== undefined ? classifyCommandResolution(effectiveCommand) : null);
|
|
280
460
|
const installationAuthorized = Boolean(
|
|
281
461
|
details?.installationAuthorized
|
|
282
462
|
|| details?.authority?.softwareInstallation === "AUTHORIZED"
|
|
@@ -289,15 +469,28 @@ export async function recordCheck({
|
|
|
289
469
|
status,
|
|
290
470
|
evidenceKind,
|
|
291
471
|
source,
|
|
472
|
+
...(executionRef === undefined ? {} : { executionRef }),
|
|
473
|
+
...(provenance === undefined ? {} : { provenance }),
|
|
292
474
|
timestamp: new Date().toISOString(),
|
|
293
|
-
...(exitCode
|
|
475
|
+
...(execution?.exitCode !== null && execution?.exitCode !== undefined
|
|
476
|
+
? { exitCode: execution.exitCode }
|
|
477
|
+
: exitCode === undefined ? {} : { exitCode }),
|
|
294
478
|
details: {
|
|
295
|
-
...(
|
|
479
|
+
...(effectiveCommand === undefined ? {} : { command: effectiveCommand }),
|
|
296
480
|
...(result === undefined ? {} : { result }),
|
|
297
481
|
...(details === undefined ? {} : details),
|
|
298
482
|
verificationCycle: state.verificationCycle ?? 1,
|
|
299
483
|
...(classification ? {
|
|
300
484
|
execution: {
|
|
485
|
+
...(details?.execution ?? {}),
|
|
486
|
+
...(execution ? {
|
|
487
|
+
executionRef: execution.executionId,
|
|
488
|
+
argv: [...execution.argv],
|
|
489
|
+
cwd: execution.cwd,
|
|
490
|
+
resolution: execution.resolution,
|
|
491
|
+
status: execution.status,
|
|
492
|
+
exitCode: execution.exitCode,
|
|
493
|
+
} : {}),
|
|
301
494
|
resolutionMode: classification.resolutionMode,
|
|
302
495
|
mayInstall: classification.mayInstall,
|
|
303
496
|
installationAuthorized,
|
|
@@ -310,6 +503,7 @@ export async function recordCheck({
|
|
|
310
503
|
packageRoot,
|
|
311
504
|
authorityContext,
|
|
312
505
|
runtimeContext,
|
|
506
|
+
requireCommandProvenance: observedCommand,
|
|
313
507
|
});
|
|
314
508
|
const evidence = createEvidence({
|
|
315
509
|
kind: evidenceKind,
|
|
@@ -349,18 +543,6 @@ export async function recordCheck({
|
|
|
349
543
|
authorityContext,
|
|
350
544
|
runtimeContext,
|
|
351
545
|
});
|
|
352
|
-
const ledger = await validateEventLedger(target, packageRoot);
|
|
353
|
-
if (!ledger.valid) {
|
|
354
|
-
const first = ledger.errors[0];
|
|
355
|
-
throw artifactError(first.code, first.message, [ARTIFACT_PATHS.events]);
|
|
356
|
-
}
|
|
357
|
-
if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
|
|
358
|
-
throw artifactError(
|
|
359
|
-
"E_PHASE_CHRONOLOGY_INVALID",
|
|
360
|
-
"record-check requires VERIFICATION_STARTED in the current task ledger",
|
|
361
|
-
[ARTIFACT_PATHS.events],
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
546
|
const coverage = coverageForRequirements(requiredEvidence, checks, {
|
|
365
547
|
target,
|
|
366
548
|
taskId: contract.value.taskId,
|
package/src/core/completion.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
2
|
-
import { requiredEvidenceForTarget } from "./completion-artifacts.js";
|
|
2
|
+
import { requiredEvidenceForTarget, validateChecksExecutionProvenance } from "./completion-artifacts.js";
|
|
3
3
|
import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger, validateStateLedgerCoherence } from "./events.js";
|
|
4
4
|
import { evaluatePreflight } from "./preflight.js";
|
|
5
5
|
import { readContract } from "./contract.js";
|
|
@@ -42,6 +42,9 @@ function repairNext(error) {
|
|
|
42
42
|
case "E_CHECK_INVALID":
|
|
43
43
|
case "E_CHECK_STATUS_CONTRADICTION":
|
|
44
44
|
return "Run forgeloop record-check with compatible observed evidence for the named requirement.";
|
|
45
|
+
case "E_COMMAND_PROVENANCE_UNATTESTED":
|
|
46
|
+
case "E_EXECUTION_REF_INVALID":
|
|
47
|
+
return "Run forgeloop run-check with the exact argv, or record the result as manual/NOT_VERIFIED evidence without claiming command execution.";
|
|
45
48
|
case "E_EVIDENCE_PARTIAL":
|
|
46
49
|
return "Run or finish the missing component checks and record observed evidence.";
|
|
47
50
|
case "E_EVIDENCE_INVALID":
|
|
@@ -216,6 +219,21 @@ export async function evaluateCompletion({ target, packageRoot, strict = false,
|
|
|
216
219
|
}
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
if (contract) {
|
|
223
|
+
errors.push(...await validateChecksExecutionProvenance(state?.checks, {
|
|
224
|
+
target,
|
|
225
|
+
packageRoot,
|
|
226
|
+
taskId: contract.value.taskId,
|
|
227
|
+
artifactPath: ARTIFACT_PATHS.state,
|
|
228
|
+
}));
|
|
229
|
+
errors.push(...await validateChecksExecutionProvenance(receipt?.value?.checks, {
|
|
230
|
+
target,
|
|
231
|
+
packageRoot,
|
|
232
|
+
taskId: contract.value.taskId,
|
|
233
|
+
artifactPath: ARTIFACT_PATHS.receipt,
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
|
|
219
237
|
if (state && !["REVIEWING", "COMPLETE"].includes(state.phase)) {
|
|
220
238
|
errors.push(issue("E_PHASE_PREREQUISITE_MISSING", `Completion requires REVIEWING or COMPLETE state, found ${state.phase}`, [ARTIFACT_PATHS.state]));
|
|
221
239
|
}
|
package/src/core/conformance.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
2
2
|
import { createEvidence } from "./evidence.js";
|
|
3
3
|
import { canonicalFingerprint } from "./artifacts.js";
|
|
4
|
+
import { evaluateContinuityConformance } from "./continuity-conformance.js";
|
|
4
5
|
|
|
5
6
|
function error(code, message, artifacts = []) {
|
|
6
7
|
return { code, message, artifacts };
|
|
@@ -49,9 +50,13 @@ export function validateTaskArtifactSet({
|
|
|
49
50
|
taskBriefs = [],
|
|
50
51
|
delegatedResults = [],
|
|
51
52
|
events = [],
|
|
53
|
+
continuity = null,
|
|
54
|
+
continuityContext = {},
|
|
52
55
|
} = {}) {
|
|
53
56
|
const errors = [];
|
|
54
57
|
const incomplete = [];
|
|
58
|
+
const continuityResult = evaluateContinuityConformance({ continuity, state, ...continuityContext });
|
|
59
|
+
errors.push(...continuityResult.errors);
|
|
55
60
|
|
|
56
61
|
addVersionErrors(route, "route", errors);
|
|
57
62
|
addVersionErrors(state, "state", errors);
|
|
@@ -122,11 +127,11 @@ export function validateTaskArtifactSet({
|
|
|
122
127
|
|
|
123
128
|
const sortedErrors = sortErrors(errors);
|
|
124
129
|
let status = "VALID";
|
|
125
|
-
if (sortedErrors.some((item) => item.code === "UNSUPPORTED_PROTOCOL_VERSION")) {
|
|
130
|
+
if (sortedErrors.some((item) => item.code === "UNSUPPORTED_PROTOCOL_VERSION") || continuityResult.status === "INVALID") {
|
|
126
131
|
status = "INVALID";
|
|
127
132
|
} else if (sortedErrors.length > 0) {
|
|
128
133
|
status = "INCONSISTENT";
|
|
129
|
-
} else if (stateClassification?.status === "REVALIDATION_REQUIRED") {
|
|
134
|
+
} else if (stateClassification?.status === "REVALIDATION_REQUIRED" || continuityResult.status === "STALE") {
|
|
130
135
|
status = "STALE";
|
|
131
136
|
} else if (incomplete.length > 0) {
|
|
132
137
|
status = "INCOMPLETE";
|
|
@@ -173,6 +178,7 @@ export function validateTaskArtifactSet({
|
|
|
173
178
|
incomplete: [...new Set(incomplete)].sort(),
|
|
174
179
|
stale,
|
|
175
180
|
delegation,
|
|
181
|
+
continuity: continuityResult,
|
|
176
182
|
evidence: [createEvidence({
|
|
177
183
|
kind: evidenceKind,
|
|
178
184
|
source: "ForgeLoop protocol conformance",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const OPTION_FIELDS = Object.freeze({
|
|
2
|
+
"--focus-id": ["continuityFocusId", false],
|
|
3
|
+
"--focus-summary": ["continuityFocusSummary", false],
|
|
4
|
+
"--remaining": ["continuityRemaining", true],
|
|
5
|
+
"--known-issue": ["continuityKnownIssues", true],
|
|
6
|
+
"--changed-area": ["continuityChangedAreas", true],
|
|
7
|
+
"--inspect-first": ["continuityInspectFirst", true],
|
|
8
|
+
"--resume-note": ["continuityResumeNote", false],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function continuityOptionDefaults() {
|
|
12
|
+
return {
|
|
13
|
+
continuityFocusId: null,
|
|
14
|
+
continuityFocusSummary: null,
|
|
15
|
+
continuityRemaining: [],
|
|
16
|
+
continuityKnownIssues: [],
|
|
17
|
+
continuityChangedAreas: [],
|
|
18
|
+
continuityInspectFirst: [],
|
|
19
|
+
continuityResumeNote: null,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function consumeContinuityOption({ argument, argv, index, options }) {
|
|
24
|
+
const definition = OPTION_FIELDS[argument];
|
|
25
|
+
if (!definition) return { handled: false, index };
|
|
26
|
+
const value = argv[index + 1];
|
|
27
|
+
if (!value || value.startsWith("-")) throw new Error(`${argument} requires a value`);
|
|
28
|
+
const [field, repeatable] = definition;
|
|
29
|
+
if (repeatable) options[field].push(value);
|
|
30
|
+
else options[field] = value;
|
|
31
|
+
return { handled: true, index: index + 1 };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function hasContinuityOptions(options = {}) {
|
|
35
|
+
return Boolean(
|
|
36
|
+
options.continuityFocusId
|
|
37
|
+
|| options.continuityFocusSummary
|
|
38
|
+
|| options.continuityResumeNote
|
|
39
|
+
|| options.continuityRemaining?.length
|
|
40
|
+
|| options.continuityKnownIssues?.length
|
|
41
|
+
|| options.continuityChangedAreas?.length
|
|
42
|
+
|| options.continuityInspectFirst?.length
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateContinuityOptions(command, options = {}) {
|
|
47
|
+
if (command !== "record-continuity" && hasContinuityOptions(options)) {
|
|
48
|
+
throw new Error(`Continuity recording options are not valid for ${command}`);
|
|
49
|
+
}
|
|
50
|
+
if (command === "record-continuity") {
|
|
51
|
+
const hasFocusId = Boolean(options.continuityFocusId);
|
|
52
|
+
const hasFocusSummary = Boolean(options.continuityFocusSummary);
|
|
53
|
+
if (hasFocusId !== hasFocusSummary) {
|
|
54
|
+
throw new Error("record-continuity requires --focus-id and --focus-summary together");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return options;
|
|
58
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { classifyContinuity } from "./continuity-reconciliation.js";
|
|
2
|
+
|
|
3
|
+
function error(code, message) {
|
|
4
|
+
return { code, message, artifacts: ["continuity", "state"] };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function evaluateContinuityConformance(input = {}) {
|
|
8
|
+
if (!input.continuity) {
|
|
9
|
+
return {
|
|
10
|
+
required: false,
|
|
11
|
+
status: "NOT_APPLICABLE",
|
|
12
|
+
classification: "ABSENT",
|
|
13
|
+
errors: [],
|
|
14
|
+
reasonCodes: [],
|
|
15
|
+
reasons: ["CONTINUITY_ABSENT"],
|
|
16
|
+
authority: "OPERATIONAL_CONTEXT_ONLY",
|
|
17
|
+
evidenceAuthority: "NONE",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const classification = classifyContinuity(input);
|
|
22
|
+
const result = {
|
|
23
|
+
required: false,
|
|
24
|
+
status: classification.classification === "FRESH"
|
|
25
|
+
? "VALID"
|
|
26
|
+
: classification.classification === "RECONCILIATION_REQUIRED"
|
|
27
|
+
? "STALE"
|
|
28
|
+
: classification.classification === "NOT_APPLICABLE"
|
|
29
|
+
? "NOT_APPLICABLE"
|
|
30
|
+
: classification.classification,
|
|
31
|
+
classification: classification.classification,
|
|
32
|
+
errors: [],
|
|
33
|
+
reasonCodes: [...classification.reasonCodes],
|
|
34
|
+
reasons: [...classification.reasons],
|
|
35
|
+
authority: "OPERATIONAL_CONTEXT_ONLY",
|
|
36
|
+
evidenceAuthority: "NONE",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (["INVALID", "INCONSISTENT"].includes(classification.classification)) {
|
|
40
|
+
result.errors = classification.reasonCodes.map((code, index) => error(
|
|
41
|
+
code,
|
|
42
|
+
classification.reasons[index] ?? `Continuity is ${classification.classification.toLowerCase()}`,
|
|
43
|
+
));
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const HEALTHY = new Set(["ABSENT", "FRESH", "NOT_APPLICABLE"]);
|
|
2
|
+
|
|
3
|
+
export function continuityIsHealthy(result) {
|
|
4
|
+
return HEALTHY.has(result?.classification ?? "ABSENT");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function continuityFinding(result) {
|
|
8
|
+
if (continuityIsHealthy(result)) return null;
|
|
9
|
+
const classification = result?.classification ?? "INVALID";
|
|
10
|
+
const severity = classification === "RECONCILIATION_REQUIRED" ? "warning" : "error";
|
|
11
|
+
return {
|
|
12
|
+
code: `continuity-${classification.toLowerCase().replaceAll("_", "-")}`,
|
|
13
|
+
severity,
|
|
14
|
+
path: result?.path ?? ".forgeloop/continuity.json",
|
|
15
|
+
message: result?.reasons?.join(", ") || `Continuity is ${classification}`,
|
|
16
|
+
remediation: classification === "RECONCILIATION_REQUIRED"
|
|
17
|
+
? "Run forgeloop reconcile-continuity, inspect the current checkout, then record corrected continuity before advancing verification."
|
|
18
|
+
: "Repair or clear continuity after reviewing the current work state and checkout.",
|
|
19
|
+
};
|
|
20
|
+
}
|