@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
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { clearContinuity } from "../core/continuity.js";
|
|
2
|
+
|
|
3
|
+
export async function runClearContinuity({ target } = {}) {
|
|
4
|
+
return clearContinuity(target);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatClearContinuityResult(result) {
|
|
8
|
+
return `${result.removed ? "cleared" : "absent"}: ${result.path}\n`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { reconcileContinuity } from "../core/continuity-reconciliation.js";
|
|
2
|
+
|
|
3
|
+
export async function runContinuity({ target, packageRoot } = {}) {
|
|
4
|
+
return reconcileContinuity({ target, packageRoot });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatContinuityResult(result) {
|
|
8
|
+
const value = result.continuity;
|
|
9
|
+
const lines = [
|
|
10
|
+
"FORGELOOP EXECUTION CONTINUITY",
|
|
11
|
+
"",
|
|
12
|
+
`classification: ${result.classification}`,
|
|
13
|
+
`task: ${value?.taskId ?? "none"}`,
|
|
14
|
+
`phase: ${value?.phase ?? "none"}`,
|
|
15
|
+
`current focus: ${value?.currentFocus?.id ?? "none"}`,
|
|
16
|
+
`remaining work: ${value?.remainingWork?.length ?? 0}`,
|
|
17
|
+
`known issues: ${value?.knownIssues?.length ?? 0}`,
|
|
18
|
+
`inspect first: ${value?.inspectFirst?.join(", ") || "none"}`,
|
|
19
|
+
"",
|
|
20
|
+
"Authority: OPERATIONAL_CONTEXT_ONLY",
|
|
21
|
+
"Evidence: NONE",
|
|
22
|
+
];
|
|
23
|
+
if (result.reasons?.length) lines.push(`Reasons: ${result.reasons.join(", ")}`);
|
|
24
|
+
return `${lines.join("\n")}\n`;
|
|
25
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertSafePath, fileExists, ensureWithin, readBytes } from "../core/filesystem.js";
|
|
1
|
+
import { assertSafePath, fileExists, ensureWithin, readBytes, writeFileAtomic } from "../core/filesystem.js";
|
|
2
2
|
import { readManifest, sha256, writeManifest } from "../core/manifest.js";
|
|
3
3
|
import { readTemplateEntries } from "../core/templates.js";
|
|
4
4
|
import { createEvidence } from "../core/evidence.js";
|
|
@@ -113,9 +113,10 @@ async function adoptAdapters({ target, manifest, adoptPaths, findings }) {
|
|
|
113
113
|
return nextManifest;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export async function runDoctor({ target, packageRoot, adoptPaths = [], strict = false }) {
|
|
116
|
+
export async function runDoctor({ target, packageRoot, adoptPaths = [], strict = false, fix = false }) {
|
|
117
117
|
const findings = [];
|
|
118
118
|
let manifest = null;
|
|
119
|
+
let manifestChanged = false;
|
|
119
120
|
try {
|
|
120
121
|
manifest = await readManifest(target);
|
|
121
122
|
if (!manifest) {
|
|
@@ -186,6 +187,16 @@ export async function runDoctor({ target, packageRoot, adoptPaths = [], strict =
|
|
|
186
187
|
));
|
|
187
188
|
continue;
|
|
188
189
|
}
|
|
190
|
+
if (fix && manifest && (layoutVersion >= LAYOUT_VERSION || managedPath.startsWith(".forgeloop/"))) {
|
|
191
|
+
await writeFileAtomic(destination, entry.bytes);
|
|
192
|
+
manifest.files[managedPath] = {
|
|
193
|
+
sha256: sha256(entry.bytes),
|
|
194
|
+
preserve: entry.sourcePath === "PROJECT_PROFILE.md",
|
|
195
|
+
};
|
|
196
|
+
manifestChanged = true;
|
|
197
|
+
findings.push(finding("file-restored", "info", managedPath, "Restored missing managed template file.", "No action required."));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
189
200
|
findings.push(finding("file-missing", "error", managedPath, "Managed file is missing."));
|
|
190
201
|
continue;
|
|
191
202
|
}
|
|
@@ -280,6 +291,10 @@ export async function runDoctor({ target, packageRoot, adoptPaths = [], strict =
|
|
|
280
291
|
}
|
|
281
292
|
}
|
|
282
293
|
|
|
294
|
+
if (manifestChanged && manifest) {
|
|
295
|
+
await writeManifest(target, manifest);
|
|
296
|
+
}
|
|
297
|
+
|
|
283
298
|
const ok = findings.every((item) => item.severity !== "error")
|
|
284
299
|
&& (!strict || findings.every((item) => item.severity !== "warning"));
|
|
285
300
|
return {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { reconcileContinuity } from "../core/continuity-reconciliation.js";
|
|
2
|
+
|
|
3
|
+
export async function runReconcileContinuity({ target, packageRoot } = {}) {
|
|
4
|
+
return reconcileContinuity({ target, packageRoot });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatReconcileContinuityResult(result) {
|
|
8
|
+
const lines = [
|
|
9
|
+
`Continuity: ${result.path ?? ".forgeloop/continuity.json"}`,
|
|
10
|
+
`Classification: ${result.classification}`,
|
|
11
|
+
`Task matches: ${result.taskMatches ?? "not-verified"}`,
|
|
12
|
+
`Work state matches: ${result.workStateMatches ?? "not-verified"}`,
|
|
13
|
+
`Contract matches: ${result.contractMatches ?? "not-verified"}`,
|
|
14
|
+
`Phase matches: ${result.phaseMatches ?? "not-verified"}`,
|
|
15
|
+
`Repository: ${result.repositoryComparison}`,
|
|
16
|
+
`Changed paths: ${result.changedPathComparison}`,
|
|
17
|
+
"Authority: OPERATIONAL_CONTEXT_ONLY",
|
|
18
|
+
"Evidence: NONE",
|
|
19
|
+
];
|
|
20
|
+
if (result.reasonCodes?.length) lines.push(`Reason codes: ${result.reasonCodes.join(", ")}`);
|
|
21
|
+
if (result.reasons?.length) lines.push(`Reasons: ${result.reasons.join(", ")}`);
|
|
22
|
+
return `${lines.join("\n")}\n`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { writeContinuity } from "../core/continuity.js";
|
|
2
|
+
|
|
3
|
+
function parseWorkItem(value, label) {
|
|
4
|
+
if (typeof value !== "string") throw new Error(`${label} must use <id>:<summary>`);
|
|
5
|
+
const separator = value.indexOf(":");
|
|
6
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
7
|
+
throw new Error(`${label} must use <id>:<summary>`);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
id: value.slice(0, separator).trim(),
|
|
11
|
+
summary: value.slice(separator + 1).trim(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runRecordContinuity({
|
|
16
|
+
target,
|
|
17
|
+
packageRoot,
|
|
18
|
+
focusId,
|
|
19
|
+
focusSummary,
|
|
20
|
+
remaining = [],
|
|
21
|
+
knownIssues = [],
|
|
22
|
+
changedAreas = [],
|
|
23
|
+
inspectFirst = [],
|
|
24
|
+
resumeNote,
|
|
25
|
+
state,
|
|
26
|
+
contract,
|
|
27
|
+
repositoryFingerprint,
|
|
28
|
+
now,
|
|
29
|
+
dryRun = false,
|
|
30
|
+
} = {}) {
|
|
31
|
+
if ((focusId && !focusSummary) || (!focusId && focusSummary)) {
|
|
32
|
+
throw new Error("record-continuity requires --focus-id and --focus-summary together");
|
|
33
|
+
}
|
|
34
|
+
return writeContinuity(target, {
|
|
35
|
+
...(focusId ? { currentFocus: { id: focusId, summary: focusSummary } } : {}),
|
|
36
|
+
remainingWork: remaining.map((item) => parseWorkItem(item, "--remaining")),
|
|
37
|
+
knownIssues: knownIssues.map((item) => parseWorkItem(item, "--known-issue")),
|
|
38
|
+
changedAreas,
|
|
39
|
+
inspectFirst,
|
|
40
|
+
...(resumeNote ? { resumeNote } : {}),
|
|
41
|
+
}, {
|
|
42
|
+
packageRoot,
|
|
43
|
+
...(state ? { state } : {}),
|
|
44
|
+
...(contract ? { contract } : {}),
|
|
45
|
+
...(repositoryFingerprint ? { repositoryFingerprint } : {}),
|
|
46
|
+
...(now ? { now } : {}),
|
|
47
|
+
dryRun,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatRecordContinuityResult(result) {
|
|
52
|
+
const value = result.value;
|
|
53
|
+
return [
|
|
54
|
+
`Continuity: ${result.path}`,
|
|
55
|
+
`Task: ${value.taskId}`,
|
|
56
|
+
`Phase: ${value.phase}`,
|
|
57
|
+
`Focus: ${value.currentFocus?.id ?? "none"}`,
|
|
58
|
+
`Remaining: ${value.remainingWork.length}`,
|
|
59
|
+
`Known issues: ${value.knownIssues.length}`,
|
|
60
|
+
"Authority: OPERATIONAL_CONTEXT_ONLY",
|
|
61
|
+
"Evidence: NONE",
|
|
62
|
+
].join("\n") + "\n";
|
|
63
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertRecordCheckPrerequisites,
|
|
3
|
+
recordCheck as recordCheckArtifact,
|
|
4
|
+
} from "../core/completion-artifacts.js";
|
|
5
|
+
import { runCommandExecution } from "../core/execution.js";
|
|
6
|
+
|
|
7
|
+
export async function runCheck({
|
|
8
|
+
target,
|
|
9
|
+
packageRoot,
|
|
10
|
+
id,
|
|
11
|
+
requirement,
|
|
12
|
+
argv,
|
|
13
|
+
details,
|
|
14
|
+
authorityContext,
|
|
15
|
+
runtimeContext,
|
|
16
|
+
}) {
|
|
17
|
+
if (typeof id !== "string" || id.trim() === "" || typeof requirement !== "string" || requirement.trim() === "") {
|
|
18
|
+
const error = new Error("run-check requires non-empty id and requirement");
|
|
19
|
+
error.code = "E_CHECK_INVALID";
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
const ready = await assertRecordCheckPrerequisites({
|
|
23
|
+
target,
|
|
24
|
+
packageRoot,
|
|
25
|
+
requirement,
|
|
26
|
+
status: "passed",
|
|
27
|
+
evidenceKind: "OBSERVED",
|
|
28
|
+
authorityContext,
|
|
29
|
+
runtimeContext,
|
|
30
|
+
});
|
|
31
|
+
const verificationCycle = ready.state.verificationCycle ?? 1;
|
|
32
|
+
const execution = await runCommandExecution({
|
|
33
|
+
target,
|
|
34
|
+
packageRoot,
|
|
35
|
+
taskId: ready.contract.value.taskId,
|
|
36
|
+
checkId: id,
|
|
37
|
+
requirement,
|
|
38
|
+
verificationCycle,
|
|
39
|
+
argv,
|
|
40
|
+
details,
|
|
41
|
+
authorityContext,
|
|
42
|
+
runtimeContext,
|
|
43
|
+
});
|
|
44
|
+
const status = execution.execution.status === "passed" ? "passed" : "failed";
|
|
45
|
+
const recorded = await recordCheckArtifact({
|
|
46
|
+
target,
|
|
47
|
+
packageRoot,
|
|
48
|
+
id,
|
|
49
|
+
kind: "command",
|
|
50
|
+
requirement,
|
|
51
|
+
status,
|
|
52
|
+
evidenceKind: "OBSERVED",
|
|
53
|
+
command: execution.execution.argv.join(" "),
|
|
54
|
+
result: execution.result,
|
|
55
|
+
...(execution.execution.exitCode === null ? {} : { exitCode: execution.execution.exitCode }),
|
|
56
|
+
details,
|
|
57
|
+
executionRef: execution.execution.executionId,
|
|
58
|
+
provenance: "FORGELOOP_EXECUTED",
|
|
59
|
+
authorityContext,
|
|
60
|
+
runtimeContext,
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
...recorded,
|
|
64
|
+
execution: execution.execution,
|
|
65
|
+
executionPath: execution.path,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatRunCheckResult(result) {
|
|
70
|
+
return [
|
|
71
|
+
"FORGELOOP CHECK EXECUTED",
|
|
72
|
+
`id: ${result.check.id}`,
|
|
73
|
+
`requirement: ${result.check.requirement}`,
|
|
74
|
+
`status: ${result.check.status}`,
|
|
75
|
+
`execution: ${result.execution.executionId}`,
|
|
76
|
+
`argv: ${result.execution.argv.join(" ")}`,
|
|
77
|
+
`exit code: ${result.execution.exitCode ?? "not-started"}`,
|
|
78
|
+
`artifact: ${result.executionPath}`,
|
|
79
|
+
`coverage: ${result.coverage.find((item) => item.requirement === result.check.requirement)?.status ?? "NOT_VERIFIED"}`,
|
|
80
|
+
`receipt: ${result.path}`,
|
|
81
|
+
"",
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { readAndClassifyWorkState } from "../core/work-state.js";
|
|
2
2
|
import { inspectSchemaHealth } from "../core/schema-validation.js";
|
|
3
|
+
import { reconcileContinuity } from "../core/continuity-reconciliation.js";
|
|
3
4
|
|
|
4
5
|
export async function runStatus({ target, packageRoot, contractFile = null }) {
|
|
5
6
|
const state = await readAndClassifyWorkState({ target, packageRoot, contractFile });
|
|
6
|
-
const protocol = await
|
|
7
|
+
const [protocol, continuity] = await Promise.all([
|
|
8
|
+
inspectSchemaHealth(target),
|
|
9
|
+
reconcileContinuity({ target, packageRoot }),
|
|
10
|
+
]);
|
|
7
11
|
return {
|
|
8
12
|
...state,
|
|
9
13
|
protocol,
|
|
14
|
+
continuity,
|
|
10
15
|
evidence: [...(state.evidence ?? []), ...(protocol.evidence ?? [])],
|
|
11
16
|
};
|
|
12
17
|
}
|
|
@@ -24,6 +29,14 @@ export function formatStatusResult(result) {
|
|
|
24
29
|
if (result.contractComparison) lines.push(`Contract: ${result.contractComparison}`);
|
|
25
30
|
if (result.artifactComparison) lines.push(`Artifacts: ${result.artifactComparison}`);
|
|
26
31
|
if (result.protocol) lines.push(`Schemas: ${result.protocol.status}`);
|
|
32
|
+
if (result.continuity) {
|
|
33
|
+
const continuity = result.continuity.continuity;
|
|
34
|
+
lines.push(`Continuity: ${result.continuity.classification}`);
|
|
35
|
+
lines.push(`Continuity focus: ${continuity?.currentFocus?.id ?? "none"}`);
|
|
36
|
+
lines.push(`Continuity remaining: ${continuity?.remainingWork?.length ?? 0}`);
|
|
37
|
+
lines.push(`Continuity known issues: ${continuity?.knownIssues?.length ?? 0}`);
|
|
38
|
+
lines.push(`Continuity authority: ${result.continuity.authority ?? "OPERATIONAL_CONTEXT_ONLY"}`);
|
|
39
|
+
}
|
|
27
40
|
if (result.error) lines.push(`Error: ${result.error}`);
|
|
28
41
|
return `${lines.join("\n")}\n`;
|
|
29
42
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -9,9 +9,8 @@ import {
|
|
|
9
9
|
writeManifest,
|
|
10
10
|
} from "../core/manifest.js";
|
|
11
11
|
import { readTemplateEntries } from "../core/templates.js";
|
|
12
|
-
import { isNativeAdapterPath, LAYOUT_VERSION } from "../core/target-layout.js";
|
|
12
|
+
import { isNativeAdapterPath, LAYOUT_VERSION, LEGACY_PROFILE_PATH } from "../core/target-layout.js";
|
|
13
13
|
|
|
14
|
-
const PROFILE_PATH = "PROJECT_PROFILE.md";
|
|
15
14
|
const LEGACY_CLEANUP_DIRECTORIES = Object.freeze(["ENG", "schemas"]);
|
|
16
15
|
|
|
17
16
|
function migrationConflict(code, path, message) {
|
|
@@ -169,7 +168,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
169
168
|
const legacyHash = legacyBytes ? sha256(legacyBytes) : null;
|
|
170
169
|
const unchangedManagedLegacy = Boolean(legacyRecord) && legacyHash === legacyRecord.sha256;
|
|
171
170
|
|
|
172
|
-
if (entry.sourcePath ===
|
|
171
|
+
if (entry.sourcePath === LEGACY_PROFILE_PATH
|
|
173
172
|
&& legacyExists
|
|
174
173
|
&& legacyRecord
|
|
175
174
|
&& currentHash === legacyHash) {
|
|
@@ -179,7 +178,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
179
178
|
continue;
|
|
180
179
|
}
|
|
181
180
|
|
|
182
|
-
if (!hiddenMatchesSource && !destinationRecord && entry.sourcePath !==
|
|
181
|
+
if (!hiddenMatchesSource && !destinationRecord && entry.sourcePath !== LEGACY_PROFILE_PATH) {
|
|
183
182
|
conflicts.push(migrationConflict(
|
|
184
183
|
"E_HIDDEN_KIT_MIGRATION_CONFLICT",
|
|
185
184
|
entry.relativePath,
|
|
@@ -191,12 +190,12 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
191
190
|
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath, legacyHash);
|
|
192
191
|
nextManifest.files[entry.relativePath] = manifestRecord(
|
|
193
192
|
sourceHash,
|
|
194
|
-
entry.sourcePath ===
|
|
193
|
+
entry.sourcePath === LEGACY_PROFILE_PATH,
|
|
195
194
|
legacyHash,
|
|
196
195
|
);
|
|
197
196
|
continue;
|
|
198
197
|
} else if (hiddenMatchesSource && legacyExists) {
|
|
199
|
-
const conflict = entry.sourcePath ===
|
|
198
|
+
const conflict = entry.sourcePath === LEGACY_PROFILE_PATH
|
|
200
199
|
? migrationConflict(
|
|
201
200
|
"E_PROFILE_MIGRATION_CONFLICT",
|
|
202
201
|
entry.legacyRelativePath,
|
|
@@ -214,7 +213,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
214
213
|
}
|
|
215
214
|
nextManifest.files[entry.relativePath] = manifestRecord(
|
|
216
215
|
currentHash,
|
|
217
|
-
entry.sourcePath ===
|
|
216
|
+
entry.sourcePath === LEGACY_PROFILE_PATH
|
|
218
217
|
|| !destinationRecord
|
|
219
218
|
|| Boolean(destinationRecord.preserve),
|
|
220
219
|
);
|
|
@@ -224,7 +223,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
224
223
|
if (!legacyExists) {
|
|
225
224
|
addAction(actions, dryRun, "create", entry.relativePath);
|
|
226
225
|
writes.push({ destination, bytes: entry.bytes });
|
|
227
|
-
nextManifest.files[entry.relativePath] = manifestRecord(sourceHash, entry.sourcePath ===
|
|
226
|
+
nextManifest.files[entry.relativePath] = manifestRecord(sourceHash, entry.sourcePath === LEGACY_PROFILE_PATH);
|
|
228
227
|
continue;
|
|
229
228
|
}
|
|
230
229
|
|
|
@@ -232,7 +231,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
232
231
|
const legacyHash = sha256(legacyBytes);
|
|
233
232
|
const unchangedManaged = Boolean(legacyRecord) && legacyHash === legacyRecord.sha256;
|
|
234
233
|
|
|
235
|
-
if (entry.sourcePath ===
|
|
234
|
+
if (entry.sourcePath === LEGACY_PROFILE_PATH && legacyRecord) {
|
|
236
235
|
addAction(actions, dryRun, "move-profile", entry.legacyRelativePath, { to: entry.relativePath });
|
|
237
236
|
writes.push({ destination, bytes: legacyBytes });
|
|
238
237
|
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath, legacyHash);
|
|
@@ -256,7 +255,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
256
255
|
continue;
|
|
257
256
|
}
|
|
258
257
|
|
|
259
|
-
const conflict = entry.sourcePath ===
|
|
258
|
+
const conflict = entry.sourcePath === LEGACY_PROFILE_PATH
|
|
260
259
|
? migrationConflict(
|
|
261
260
|
"E_PROFILE_MIGRATION_CONFLICT",
|
|
262
261
|
entry.legacyRelativePath,
|
|
@@ -275,7 +274,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
275
274
|
reason: legacyRecord ? "managed-modified" : "unmanaged",
|
|
276
275
|
});
|
|
277
276
|
writes.push({ destination, bytes: entry.bytes });
|
|
278
|
-
nextManifest.files[entry.relativePath] = manifestRecord(sourceHash, entry.sourcePath ===
|
|
277
|
+
nextManifest.files[entry.relativePath] = manifestRecord(sourceHash, entry.sourcePath === LEGACY_PROFILE_PATH);
|
|
279
278
|
}
|
|
280
279
|
|
|
281
280
|
// Apply all hidden writes and verify their bytes before changing manifest authority.
|
|
@@ -364,7 +363,7 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion, h
|
|
|
364
363
|
entry,
|
|
365
364
|
record: {
|
|
366
365
|
sha256: sourceHash,
|
|
367
|
-
preserve: entry.sourcePath ===
|
|
366
|
+
preserve: entry.sourcePath === LEGACY_PROFILE_PATH,
|
|
368
367
|
...(record?.legacySha256 ? { legacySha256: record.legacySha256 } : {}),
|
|
369
368
|
},
|
|
370
369
|
});
|
|
@@ -376,7 +375,7 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion, h
|
|
|
376
375
|
continue;
|
|
377
376
|
}
|
|
378
377
|
|
|
379
|
-
if (record.preserve || entry.sourcePath ===
|
|
378
|
+
if (record.preserve || entry.sourcePath === LEGACY_PROFILE_PATH) {
|
|
380
379
|
actions.push({ action: "skip", path: entry.relativePath, reason: "preserved" });
|
|
381
380
|
continue;
|
|
382
381
|
}
|
|
@@ -9,6 +9,9 @@ import { validateTaskBrief, validateDelegatedResult } from "../core/delegation.j
|
|
|
9
9
|
import { ARTIFACT_PATHS, readJsonArtifact } from "../core/artifacts.js";
|
|
10
10
|
import { evaluatePreflight, validateReadyProtocolConsistency } from "../core/preflight.js";
|
|
11
11
|
import { validateEventLedger, validateStateLedgerCoherence } from "../core/events.js";
|
|
12
|
+
import { validateChecksExecutionProvenance } from "../core/completion-artifacts.js";
|
|
13
|
+
import { assertContinuitySemantics } from "../core/continuity.js";
|
|
14
|
+
import { currentChangedPaths, currentRepositoryFingerprint } from "../core/repository.js";
|
|
12
15
|
|
|
13
16
|
async function readArtifact(target, relativePath, label) {
|
|
14
17
|
if (!relativePath) return null;
|
|
@@ -47,6 +50,7 @@ export async function runValidateProtocol({
|
|
|
47
50
|
stateFile = null,
|
|
48
51
|
receiptFile = null,
|
|
49
52
|
contractFile = null,
|
|
53
|
+
continuityFile = null,
|
|
50
54
|
taskBriefFiles = [],
|
|
51
55
|
delegatedResultFiles = [],
|
|
52
56
|
}) {
|
|
@@ -54,6 +58,7 @@ export async function runValidateProtocol({
|
|
|
54
58
|
["route", routeFile],
|
|
55
59
|
["state", stateFile],
|
|
56
60
|
["receipt", receiptFile],
|
|
61
|
+
["continuity", continuityFile],
|
|
57
62
|
...taskBriefFiles.map((file) => [`task brief:${file}`, file]),
|
|
58
63
|
...delegatedResultFiles.map((file) => [`delegated result:${file}`, file]),
|
|
59
64
|
];
|
|
@@ -67,6 +72,7 @@ export async function runValidateProtocol({
|
|
|
67
72
|
const route = loaded.find((item) => item.label === "route")?.value ?? null;
|
|
68
73
|
const state = loaded.find((item) => item.label === "state")?.value ?? null;
|
|
69
74
|
const receipt = loaded.find((item) => item.label === "receipt")?.value ?? null;
|
|
75
|
+
const continuity = loaded.find((item) => item.label === "continuity")?.value ?? null;
|
|
70
76
|
const taskBriefs = loaded.filter((item) => item.label.startsWith("task brief:") && item.value).map((item) => item.value);
|
|
71
77
|
const delegatedResults = loaded.filter((item) => item.label.startsWith("delegated result:") && item.value).map((item) => item.value);
|
|
72
78
|
const schemaErrors = [];
|
|
@@ -87,6 +93,24 @@ export async function runValidateProtocol({
|
|
|
87
93
|
await validateLoaded(loaded.find((item) => item.label === "route"), "routing-result", async (value) => assertRouteInvariants(value));
|
|
88
94
|
await validateLoaded(loaded.find((item) => item.label === "state"), "work-state", async (value) => assertWorkStateSemantics(value));
|
|
89
95
|
await validateLoaded(loaded.find((item) => item.label === "receipt"), "execution-receipt", async (value) => validateReceipt(value, packageRoot));
|
|
96
|
+
await validateLoaded(loaded.find((item) => item.label === "continuity"), "continuity", async (value) => assertContinuitySemantics(value));
|
|
97
|
+
for (const [value, artifactPath] of [
|
|
98
|
+
[state, stateFile],
|
|
99
|
+
[receipt, receiptFile],
|
|
100
|
+
]) {
|
|
101
|
+
if (!value) continue;
|
|
102
|
+
const provenanceErrors = await validateChecksExecutionProvenance(value.checks, {
|
|
103
|
+
target,
|
|
104
|
+
packageRoot,
|
|
105
|
+
taskId: value.taskId,
|
|
106
|
+
artifactPath,
|
|
107
|
+
});
|
|
108
|
+
schemaErrors.push(...provenanceErrors.map((error) => ({
|
|
109
|
+
...error,
|
|
110
|
+
message: `Command provenance validation failed: ${error.message}`,
|
|
111
|
+
artifacts: [artifactPath, ...(error.artifacts ?? [])],
|
|
112
|
+
})));
|
|
113
|
+
}
|
|
90
114
|
for (const item of loaded.filter((candidate) => candidate.label.startsWith("task brief:"))) {
|
|
91
115
|
await validateLoaded(item, "task-brief", async (value) => validateTaskBrief(value, packageRoot));
|
|
92
116
|
}
|
|
@@ -124,11 +148,20 @@ export async function runValidateProtocol({
|
|
|
124
148
|
})),
|
|
125
149
|
];
|
|
126
150
|
}
|
|
151
|
+
const continuityContext = continuity && state && !stateValidationError
|
|
152
|
+
? {
|
|
153
|
+
contractFingerprint: state.contractFingerprint,
|
|
154
|
+
repositoryFingerprint: await currentRepositoryFingerprint(target),
|
|
155
|
+
changedPaths: await currentChangedPaths(target),
|
|
156
|
+
}
|
|
157
|
+
: {};
|
|
127
158
|
const result = validateTaskArtifactSet({
|
|
128
159
|
route,
|
|
129
160
|
state,
|
|
130
161
|
stateClassification,
|
|
131
162
|
receipt,
|
|
163
|
+
continuity,
|
|
164
|
+
continuityContext,
|
|
132
165
|
taskBriefs,
|
|
133
166
|
delegatedResults,
|
|
134
167
|
events: ledgerEvents,
|
|
@@ -146,6 +179,10 @@ export async function runValidateProtocol({
|
|
|
146
179
|
|
|
147
180
|
export function formatValidateProtocolResult(result) {
|
|
148
181
|
const lines = [`Protocol: ${result.status}`];
|
|
182
|
+
if (result.continuity) {
|
|
183
|
+
lines.push(`Continuity: ${result.continuity.status}`);
|
|
184
|
+
lines.push(`Continuity authority: ${result.continuity.authority}`);
|
|
185
|
+
}
|
|
149
186
|
if (result.stale) {
|
|
150
187
|
lines.push(`Repository: ${result.stale.repositoryComparison}`);
|
|
151
188
|
lines.push(`Contract: ${result.stale.contractComparison}`);
|
package/src/core/artifacts.js
CHANGED
|
@@ -16,9 +16,18 @@ export const ARTIFACT_PATHS = Object.freeze({
|
|
|
16
16
|
config: ".forgeloop/config.json",
|
|
17
17
|
gates: ".forgeloop/gates",
|
|
18
18
|
state: ".forgeloop/work-state.json",
|
|
19
|
+
continuity: ".forgeloop/continuity.json",
|
|
19
20
|
receipt: ".forgeloop/execution-receipt.json",
|
|
21
|
+
executionDirectory: ".forgeloop/executions",
|
|
20
22
|
});
|
|
21
23
|
|
|
24
|
+
export function executionArtifactPath(executionId) {
|
|
25
|
+
if (typeof executionId !== "string" || !/^exec-[A-Za-z0-9_-]+$/.test(executionId)) {
|
|
26
|
+
throw new ArtifactError("E_EXECUTION_REF_INVALID", "Execution reference must be a simple execution ID");
|
|
27
|
+
}
|
|
28
|
+
return `${ARTIFACT_PATHS.executionDirectory}/${executionId}.json`;
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
export class ArtifactError extends Error {
|
|
23
32
|
constructor(code, message, artifacts = []) {
|
|
24
33
|
super(message);
|
package/src/core/bundles.js
CHANGED
|
@@ -4,6 +4,9 @@ import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts
|
|
|
4
4
|
import { readContract, validateContract } from "./contract.js";
|
|
5
5
|
import { assertSafePath, ensureWithin, fileExists, readBytes, writeFileAtomic } from "./filesystem.js";
|
|
6
6
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
7
|
+
import { validateChecksExecutionProvenance } from "./completion-artifacts.js";
|
|
8
|
+
import { readExecutionArtifact } from "./execution.js";
|
|
9
|
+
import { assertContinuitySemantics } from "./continuity.js";
|
|
7
10
|
|
|
8
11
|
export const BUNDLE_SCHEMA_VERSION = 1;
|
|
9
12
|
const BUNDLE_ROOT = ".forgeloop/tasks";
|
|
@@ -37,6 +40,34 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
|
|
|
37
40
|
safeTaskId(taskId);
|
|
38
41
|
const directory = bundleDirectory(taskId);
|
|
39
42
|
const artifacts = [];
|
|
43
|
+
const stateSource = await readJsonArtifact(target, ARTIFACT_PATHS.state, "work-state", packageRoot);
|
|
44
|
+
let receiptSource = null;
|
|
45
|
+
try {
|
|
46
|
+
receiptSource = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
49
|
+
}
|
|
50
|
+
const provenanceErrors = [
|
|
51
|
+
...(await validateChecksExecutionProvenance(stateSource.value.checks, {
|
|
52
|
+
target,
|
|
53
|
+
packageRoot,
|
|
54
|
+
taskId,
|
|
55
|
+
artifactPath: ARTIFACT_PATHS.state,
|
|
56
|
+
})),
|
|
57
|
+
...(await validateChecksExecutionProvenance(receiptSource?.value?.checks, {
|
|
58
|
+
target,
|
|
59
|
+
packageRoot,
|
|
60
|
+
taskId,
|
|
61
|
+
artifactPath: ARTIFACT_PATHS.receipt,
|
|
62
|
+
})),
|
|
63
|
+
];
|
|
64
|
+
if (provenanceErrors.length > 0) {
|
|
65
|
+
const first = provenanceErrors[0];
|
|
66
|
+
const error = new Error(first.message);
|
|
67
|
+
error.code = first.code;
|
|
68
|
+
error.artifacts = first.artifacts;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
40
71
|
const required = [
|
|
41
72
|
[ARTIFACT_PATHS.contract, "contract.json", "current-contract"],
|
|
42
73
|
[ARTIFACT_PATHS.route, "route.json", "routing-result"],
|
|
@@ -59,11 +90,22 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
|
|
|
59
90
|
[ARTIFACT_PATHS.receipt, "receipt.json", "execution-receipt"],
|
|
60
91
|
[ARTIFACT_PATHS.sources, "sources.json", "source-registry"],
|
|
61
92
|
[ARTIFACT_PATHS.config, "config.json", "config"],
|
|
93
|
+
[ARTIFACT_PATHS.continuity, "continuity.json", "continuity"],
|
|
62
94
|
];
|
|
63
95
|
for (const [sourcePath, destinationName, schemaName] of optional) {
|
|
64
96
|
const copied = await copyJson(target, sourcePath, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
|
|
65
97
|
if (copied && !artifacts.includes(destinationName)) artifacts.push(destinationName);
|
|
66
98
|
}
|
|
99
|
+
const executionRefs = [...new Set([
|
|
100
|
+
...(stateSource.value.checks ?? []),
|
|
101
|
+
...(receiptSource?.value?.checks ?? []),
|
|
102
|
+
].map((check) => check?.executionRef).filter(Boolean))].sort();
|
|
103
|
+
for (const executionRef of executionRefs) {
|
|
104
|
+
const execution = await readExecutionArtifact({ target, executionRef, packageRoot });
|
|
105
|
+
const destination = `${directory}/executions/${execution.value.executionId}.json`;
|
|
106
|
+
await writeJsonArtifact(target, destination, execution.value, "execution", packageRoot);
|
|
107
|
+
artifacts.push(`executions/${execution.value.executionId}.json`);
|
|
108
|
+
}
|
|
67
109
|
const eventsPath = ensureWithin(target, ARTIFACT_PATHS.events);
|
|
68
110
|
if (await fileExists(eventsPath)) {
|
|
69
111
|
await assertSafePath(target, `${directory}/events.ndjson`);
|
|
@@ -106,15 +148,51 @@ export async function readTaskBundle(target, taskId, packageRoot) {
|
|
|
106
148
|
"receipt.json": ["receipt", "execution-receipt"],
|
|
107
149
|
"sources.json": ["sources", "source-registry"],
|
|
108
150
|
"config.json": ["config", "config"],
|
|
151
|
+
"continuity.json": ["continuity", "continuity"],
|
|
109
152
|
};
|
|
153
|
+
const executions = {};
|
|
110
154
|
for (const artifact of manifest.value.artifacts) {
|
|
155
|
+
if (artifact.startsWith("executions/") && artifact.endsWith(".json")) {
|
|
156
|
+
const execution = await readJsonArtifact(target, `${directory}/${artifact}`, "execution", packageRoot);
|
|
157
|
+
executions[execution.value.executionId] = execution.value;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
111
160
|
const mapping = mappings[artifact];
|
|
112
161
|
if (!mapping) continue;
|
|
113
162
|
const loadedArtifact = await readJsonArtifact(target, `${directory}/${artifact}`, mapping[1], packageRoot);
|
|
114
163
|
if (mapping[1] === "current-contract") {
|
|
115
164
|
await validateContract(loadedArtifact.value, packageRoot);
|
|
116
165
|
}
|
|
166
|
+
if (mapping[1] === "continuity") {
|
|
167
|
+
assertContinuitySemantics(loadedArtifact.value);
|
|
168
|
+
}
|
|
117
169
|
loaded[mapping[0]] = loadedArtifact.value;
|
|
118
170
|
}
|
|
171
|
+
if (Object.keys(executions).length > 0) loaded.executions = executions;
|
|
172
|
+
const provenanceErrors = [
|
|
173
|
+
...(await validateChecksExecutionProvenance(loaded.state?.checks, {
|
|
174
|
+
target,
|
|
175
|
+
packageRoot,
|
|
176
|
+
taskId,
|
|
177
|
+
executionArtifacts: executions,
|
|
178
|
+
allowForeignCwd: true,
|
|
179
|
+
artifactPath: "state.json",
|
|
180
|
+
})),
|
|
181
|
+
...(await validateChecksExecutionProvenance(loaded.receipt?.checks, {
|
|
182
|
+
target,
|
|
183
|
+
packageRoot,
|
|
184
|
+
taskId,
|
|
185
|
+
executionArtifacts: executions,
|
|
186
|
+
allowForeignCwd: true,
|
|
187
|
+
artifactPath: "receipt.json",
|
|
188
|
+
})),
|
|
189
|
+
];
|
|
190
|
+
if (provenanceErrors.length > 0) {
|
|
191
|
+
const first = provenanceErrors[0];
|
|
192
|
+
const error = new Error(first.message);
|
|
193
|
+
error.code = first.code;
|
|
194
|
+
error.artifacts = first.artifacts;
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
119
197
|
return { manifest: manifest.value, artifacts: loaded };
|
|
120
198
|
}
|
package/src/core/checks.js
CHANGED
|
@@ -4,6 +4,7 @@ import { validateVerificationAuthority } from "./verification-capability.js";
|
|
|
4
4
|
export const CHECK_SCHEMA_VERSION = 1;
|
|
5
5
|
export const CHECK_STATUSES = Object.freeze(["passed", "failed", "blocked", "not-run"]);
|
|
6
6
|
export const CHECK_EVIDENCE_KINDS = Object.freeze(["OBSERVED", "INFERRED", "NOT_VERIFIED", "BLOCKED"]);
|
|
7
|
+
export const CHECK_PROVENANCE = Object.freeze(["FORGELOOP_EXECUTED", "ACTOR_REPORTED", "MANUAL_OBSERVATION"]);
|
|
7
8
|
|
|
8
9
|
function checkError(code, message, artifacts = []) {
|
|
9
10
|
const error = new Error(message);
|
|
@@ -60,6 +61,8 @@ export function createCheck(input = {}, options = {}) {
|
|
|
60
61
|
...(input.timestamp !== undefined ? { timestamp: input.timestamp } : {}),
|
|
61
62
|
...(input.repositoryFingerprint !== undefined ? { repositoryFingerprint: input.repositoryFingerprint } : {}),
|
|
62
63
|
...(input.viewport !== undefined ? { viewport: structuredClone(input.viewport) } : {}),
|
|
64
|
+
...(input.executionRef !== undefined ? { executionRef: input.executionRef } : {}),
|
|
65
|
+
...(input.provenance !== undefined ? { provenance: input.provenance } : {}),
|
|
63
66
|
...(input.details !== undefined ? { details: structuredClone(input.details) } : {}),
|
|
64
67
|
};
|
|
65
68
|
return assertCheck(check, "check", options);
|
|
@@ -76,6 +79,19 @@ export function assertCheck(value, label = "check", options = {}) {
|
|
|
76
79
|
string(value.kind, `${label}.kind`);
|
|
77
80
|
string(value.requirement, `${label}.requirement`);
|
|
78
81
|
string(value.source, `${label}.source`);
|
|
82
|
+
optionalString(value.executionRef, `${label}.executionRef`);
|
|
83
|
+
if (value.provenance !== undefined && !CHECK_PROVENANCE.includes(value.provenance)) {
|
|
84
|
+
throw checkError("E_CHECK_INVALID", `${label}.provenance must be one of ${CHECK_PROVENANCE.join(", ")}`);
|
|
85
|
+
}
|
|
86
|
+
if (options.requireCommandProvenance === true
|
|
87
|
+
&& value.kind === "command"
|
|
88
|
+
&& value.evidenceKind === "OBSERVED"
|
|
89
|
+
&& (value.executionRef === undefined || value.provenance !== "FORGELOOP_EXECUTED")) {
|
|
90
|
+
throw checkError(
|
|
91
|
+
"E_COMMAND_PROVENANCE_UNATTESTED",
|
|
92
|
+
`${label} observed command evidence requires ForgeLoop execution provenance`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
79
95
|
if (!CHECK_STATUSES.includes(value.status)) {
|
|
80
96
|
throw checkError("E_CHECK_INVALID", `${label}.status must be one of ${CHECK_STATUSES.join(", ")}`);
|
|
81
97
|
}
|