@cassiomc1/forgeloop 0.1.0 → 0.1.2
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 +7 -1
- package/.github/copilot-instructions.md +6 -0
- package/AGENTS.md +9 -1
- package/AGENT_COMPATIBILITY.md +14 -1
- package/CLAUDE.md +7 -1
- package/ENG/accessibility-eng.md +5 -0
- package/ENG/clean-code-eng.md +1 -0
- package/ENG/design-code-eng.md +5 -0
- package/ENG/games-code-design-web-eng.md +6 -0
- package/ENG/perf-code-eng.md +3 -0
- package/ENG/premium-sites-studio-eng.md +9 -0
- package/ENG/sec-code-eng.md +5 -0
- package/ENG/test-code-eng.md +3 -0
- package/GUIDE_ROUTER.md +16 -0
- package/LOOP_ENGINEERING.md +139 -4
- package/ORCHESTRATOR_INTEGRATION.md +15 -0
- package/QUALITY_SCORECARD.md +4 -0
- package/README.md +43 -1
- package/THREAT_MODEL.md +6 -0
- package/conformance/README.md +42 -0
- package/conformance/backend-auth/EXPECTED_ROUTE.json +7 -0
- package/conformance/backend-auth/REQUEST.md +4 -0
- package/conformance/backend-auth/REQUIRED_EVIDENCE.json +3 -0
- package/conformance/backend-auth/REQUIRED_GATES.json +3 -0
- package/conformance/blind-premium-website/EXPECTED_ROUTE.json +7 -0
- package/conformance/blind-premium-website/REQUEST.md +6 -0
- package/conformance/blind-premium-website/REQUIRED_EVIDENCE.json +14 -0
- package/conformance/blind-premium-website/REQUIRED_GATES.json +3 -0
- package/conformance/complete-website/EXPECTED_ROUTE.json +7 -0
- package/conformance/complete-website/REQUEST.md +6 -0
- package/conformance/complete-website/REQUIRED_EVIDENCE.json +3 -0
- package/conformance/complete-website/REQUIRED_GATES.json +3 -0
- package/conformance/docs-only/EXPECTED_ROUTE.json +7 -0
- package/conformance/docs-only/REQUEST.md +4 -0
- package/conformance/docs-only/REQUIRED_EVIDENCE.json +3 -0
- package/conformance/docs-only/REQUIRED_GATES.json +3 -0
- package/conformance/runs/2026-08-11-codex-first-live.md +98 -0
- package/conformance/runs/2026-08-11-codex-second-live.md +87 -0
- package/conformance/simple-bug/EXPECTED_ROUTE.json +7 -0
- package/conformance/simple-bug/REQUEST.md +4 -0
- package/conformance/simple-bug/REQUIRED_EVIDENCE.json +3 -0
- package/conformance/simple-bug/REQUIRED_GATES.json +3 -0
- package/package.json +5 -4
- package/schemas/activation.schema.json +15 -0
- package/schemas/check.schema.json +23 -0
- package/schemas/config.schema.json +16 -0
- package/schemas/current-contract.schema.json +50 -0
- package/schemas/event.schema.json +20 -0
- package/schemas/evidence-coverage.schema.json +17 -0
- package/schemas/execution-receipt.schema.json +10 -0
- package/schemas/gate.schema.json +32 -0
- package/schemas/policy.schema.json +19 -0
- package/schemas/preflight.schema.json +23 -0
- package/schemas/routing-result.schema.json +1 -0
- package/schemas/source-registry.schema.json +29 -0
- package/schemas/task-bundle.schema.json +14 -0
- package/schemas/work-state.schema.json +7 -1
- package/src/cli.js +230 -9
- package/src/commands/activate.js +11 -0
- package/src/commands/advance.js +12 -0
- package/src/commands/audit.js +12 -0
- package/src/commands/bundle.js +10 -0
- package/src/commands/complete.js +18 -0
- package/src/commands/policy.js +19 -0
- package/src/commands/preflight.js +12 -0
- package/src/commands/prepare-completion.js +18 -0
- package/src/commands/record-check.js +20 -0
- package/src/commands/report.js +10 -0
- package/src/commands/route.js +14 -2
- package/src/commands/validate-receipt.js +7 -6
- package/src/core/activation.js +16 -0
- package/src/core/artifacts.js +132 -0
- package/src/core/audit.js +88 -0
- package/src/core/bundles.js +120 -0
- package/src/core/checks.js +115 -0
- package/src/core/completion-artifacts.js +281 -0
- package/src/core/completion.js +295 -0
- package/src/core/config.js +35 -0
- package/src/core/conformance.js +7 -0
- package/src/core/contract.js +141 -0
- package/src/core/coverage.js +89 -0
- package/src/core/events.js +124 -0
- package/src/core/gate-artifact.js +54 -0
- package/src/core/gates.js +55 -0
- package/src/core/guide-metadata.js +62 -0
- package/src/core/phase.js +88 -0
- package/src/core/policies.js +69 -0
- package/src/core/preflight.js +246 -0
- package/src/core/profile.js +48 -0
- package/src/core/protocol.js +34 -0
- package/src/core/receipt.js +40 -1
- package/src/core/report.js +49 -0
- package/src/core/repository.js +18 -0
- package/src/core/route-artifact.js +33 -0
- package/src/core/router.js +16 -8
- package/src/core/schema-validation.js +11 -0
- package/src/core/sources.js +86 -0
- package/src/core/templates.js +22 -4
- package/src/core/work-state.js +41 -13
- /package/.forgeloop/{.gitignore → forgeloop.gitignore} +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { assertSafePath, ensureWithin, fileExists, readBytes, writeFileAtomic } from "./filesystem.js";
|
|
4
|
+
import { assertSecretFree } from "./receipt.js";
|
|
5
|
+
import { assertJsonBytes, assertJsonLimits } from "./json-safety.js";
|
|
6
|
+
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
7
|
+
import { getPackageRoot } from "./templates.js";
|
|
8
|
+
|
|
9
|
+
export const ARTIFACT_PATHS = Object.freeze({
|
|
10
|
+
contract: ".forgeloop/current-contract.json",
|
|
11
|
+
route: ".forgeloop/routing-result.json",
|
|
12
|
+
preflight: ".forgeloop/preflight.json",
|
|
13
|
+
sources: ".forgeloop/sources.json",
|
|
14
|
+
events: ".forgeloop/events.ndjson",
|
|
15
|
+
session: ".forgeloop/session.json",
|
|
16
|
+
config: ".forgeloop/config.json",
|
|
17
|
+
gates: ".forgeloop/gates",
|
|
18
|
+
state: ".forgeloop/work-state.json",
|
|
19
|
+
receipt: ".forgeloop/execution-receipt.json",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export class ArtifactError extends Error {
|
|
23
|
+
constructor(code, message, artifacts = []) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "ArtifactError";
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.artifacts = artifacts;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function canonicalize(value) {
|
|
32
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
33
|
+
if (value && typeof value === "object") {
|
|
34
|
+
return Object.fromEntries(
|
|
35
|
+
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function canonicalFingerprint(value) {
|
|
42
|
+
return createHash("sha256")
|
|
43
|
+
.update(JSON.stringify(canonicalize(value)))
|
|
44
|
+
.digest("hex");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function artifactError(code, relativePath, error) {
|
|
48
|
+
if (error instanceof ArtifactError) return error;
|
|
49
|
+
return new ArtifactError(
|
|
50
|
+
code,
|
|
51
|
+
`${relativePath}: ${error.message}`,
|
|
52
|
+
[relativePath],
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function readJsonArtifact(
|
|
57
|
+
target,
|
|
58
|
+
relativePath,
|
|
59
|
+
schemaName,
|
|
60
|
+
packageRoot = getPackageRoot(),
|
|
61
|
+
) {
|
|
62
|
+
try {
|
|
63
|
+
await assertSafePath(target, relativePath);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw artifactError("ARTIFACT_PATH_INVALID", relativePath, error);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const artifactPath = ensureWithin(target, relativePath);
|
|
69
|
+
if (!(await fileExists(artifactPath))) {
|
|
70
|
+
throw new ArtifactError(
|
|
71
|
+
"ARTIFACT_MISSING",
|
|
72
|
+
`Artifact is missing: ${relativePath}`,
|
|
73
|
+
[relativePath],
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let value;
|
|
78
|
+
try {
|
|
79
|
+
const bytes = await readBytes(artifactPath);
|
|
80
|
+
assertJsonBytes(bytes, relativePath);
|
|
81
|
+
value = JSON.parse(bytes.toString("utf8"));
|
|
82
|
+
assertJsonLimits(value, relativePath);
|
|
83
|
+
const schema = await readSchema(schemaName, packageRoot);
|
|
84
|
+
assertSchema(value, schema, relativePath);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw artifactError(
|
|
87
|
+
["JSON_LIMIT_EXCEEDED", "ARTIFACT_PATH_INVALID"].includes(error.code)
|
|
88
|
+
? error.code
|
|
89
|
+
: "ARTIFACT_INVALID",
|
|
90
|
+
relativePath,
|
|
91
|
+
error,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
value,
|
|
97
|
+
path: relativePath,
|
|
98
|
+
fingerprint: canonicalFingerprint(value),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function writeJsonArtifact(
|
|
103
|
+
target,
|
|
104
|
+
relativePath,
|
|
105
|
+
value,
|
|
106
|
+
schemaName,
|
|
107
|
+
packageRoot = getPackageRoot(),
|
|
108
|
+
{ dryRun = false } = {},
|
|
109
|
+
) {
|
|
110
|
+
try {
|
|
111
|
+
await assertSafePath(target, relativePath);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
throw artifactError("ARTIFACT_PATH_INVALID", relativePath, error);
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const artifactPath = ensureWithin(target, relativePath);
|
|
117
|
+
assertSecretFree(value);
|
|
118
|
+
const schema = await readSchema(schemaName, packageRoot);
|
|
119
|
+
assertSchema(value, schema, relativePath);
|
|
120
|
+
assertJsonLimits(value, relativePath);
|
|
121
|
+
const serialized = `${JSON.stringify(value, null, 2)}\n`;
|
|
122
|
+
assertJsonBytes(serialized, relativePath);
|
|
123
|
+
await writeFileAtomic(artifactPath, serialized, { dryRun });
|
|
124
|
+
return { path: relativePath, fingerprint: canonicalFingerprint(value), value };
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw artifactError(
|
|
127
|
+
error.code === "JSON_LIMIT_EXCEEDED" ? error.code : "ARTIFACT_INVALID",
|
|
128
|
+
relativePath,
|
|
129
|
+
error,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ARTIFACT_PATHS } from "./artifacts.js";
|
|
2
|
+
import { evaluateCompletion } from "./completion.js";
|
|
3
|
+
import { readManifest } from "./manifest.js";
|
|
4
|
+
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
5
|
+
import { readJsonArtifact } from "./artifacts.js";
|
|
6
|
+
import { currentChangedPaths } from "./repository.js";
|
|
7
|
+
|
|
8
|
+
function sortErrors(errors) {
|
|
9
|
+
return [...errors].sort((left, right) => left.code.localeCompare(right.code)
|
|
10
|
+
|| left.message.localeCompare(right.message));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function compareChangedPaths(target, packageRoot) {
|
|
14
|
+
let receipt;
|
|
15
|
+
try {
|
|
16
|
+
receipt = (await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot)).value;
|
|
17
|
+
} catch {
|
|
18
|
+
return { status: "NOT_VERIFIED", expected: [], observed: [], missing: [], unexpected: [] };
|
|
19
|
+
}
|
|
20
|
+
const observed = await currentChangedPaths(target);
|
|
21
|
+
if (observed === null) {
|
|
22
|
+
return { status: "NOT_VERIFIED", expected: receipt.changedPaths ?? [], observed: [], missing: [], unexpected: [] };
|
|
23
|
+
}
|
|
24
|
+
const expected = [...new Set(receipt.changedPaths ?? [])].sort();
|
|
25
|
+
const missing = expected.filter((relativePath) => !observed.includes(relativePath));
|
|
26
|
+
const unexpected = observed.filter((relativePath) => !expected.includes(relativePath));
|
|
27
|
+
return {
|
|
28
|
+
status: missing.length === 0 && unexpected.length === 0 ? "MATCH" : "MISMATCH",
|
|
29
|
+
expected,
|
|
30
|
+
observed,
|
|
31
|
+
missing,
|
|
32
|
+
unexpected,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function evaluateAudit({ target, packageRoot, strict = false } = {}) {
|
|
37
|
+
const completion = await evaluateCompletion({ target, packageRoot, strict });
|
|
38
|
+
let manifest = null;
|
|
39
|
+
let manifestError = null;
|
|
40
|
+
try {
|
|
41
|
+
manifest = await readManifest(target);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
manifestError = error.message;
|
|
44
|
+
}
|
|
45
|
+
const errors = sortErrors(completion.errors);
|
|
46
|
+
const changedPaths = await compareChangedPaths(target, packageRoot);
|
|
47
|
+
if (changedPaths.status === "MISMATCH") {
|
|
48
|
+
errors.push({
|
|
49
|
+
code: "E_RECEIPT_PATH_MISMATCH",
|
|
50
|
+
message: "Receipt changedPaths do not match observed repository paths",
|
|
51
|
+
artifacts: [ARTIFACT_PATHS.receipt],
|
|
52
|
+
missing: changedPaths.missing,
|
|
53
|
+
unexpected: changedPaths.unexpected,
|
|
54
|
+
next: "Run forgeloop prepare-completion to refresh changed paths, then rerun audit.",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const stale = errors.some((error) => error.code.includes("STALE") || error.code === "E_PHASE_ARTIFACT_STALE");
|
|
58
|
+
const blocked = errors.some((error) => ["E_GATE_UNVERIFIED", "E_GATE_STALE", "E_EVIDENCE_COVERAGE_PARTIAL", "E_PHASE_PREREQUISITE_MISSING"].includes(error.code));
|
|
59
|
+
const status = errors.length === 0 && completion.status === "VALID"
|
|
60
|
+
? "VALID"
|
|
61
|
+
: stale
|
|
62
|
+
? "STALE"
|
|
63
|
+
: blocked
|
|
64
|
+
? "INCOMPLETE"
|
|
65
|
+
: "INVALID";
|
|
66
|
+
return {
|
|
67
|
+
schemaVersion: 1,
|
|
68
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
69
|
+
status,
|
|
70
|
+
strict,
|
|
71
|
+
errors,
|
|
72
|
+
warnings: [...completion.warnings, ...(manifestError ? [{ code: "E_INSTALLATION_INVALID", message: manifestError }] : [])],
|
|
73
|
+
installation: {
|
|
74
|
+
status: manifest ? "ready" : manifestError ? "invalid" : "missing",
|
|
75
|
+
manifest: Boolean(manifest),
|
|
76
|
+
},
|
|
77
|
+
completion,
|
|
78
|
+
changedPaths,
|
|
79
|
+
publicationStatus: completion.publicationStatus,
|
|
80
|
+
productionReadiness: completion.productionReadiness,
|
|
81
|
+
artifacts: {
|
|
82
|
+
contract: ARTIFACT_PATHS.contract,
|
|
83
|
+
route: ARTIFACT_PATHS.route,
|
|
84
|
+
state: ARTIFACT_PATHS.state,
|
|
85
|
+
receipt: ARTIFACT_PATHS.receipt,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
4
|
+
import { readContract, validateContract } from "./contract.js";
|
|
5
|
+
import { assertSafePath, ensureWithin, fileExists, readBytes, writeFileAtomic } from "./filesystem.js";
|
|
6
|
+
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
7
|
+
|
|
8
|
+
export const BUNDLE_SCHEMA_VERSION = 1;
|
|
9
|
+
const BUNDLE_ROOT = ".forgeloop/tasks";
|
|
10
|
+
|
|
11
|
+
function safeTaskId(taskId) {
|
|
12
|
+
if (typeof taskId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(taskId)) {
|
|
13
|
+
const error = new Error(`Invalid task ID for bundle: ${taskId}`);
|
|
14
|
+
error.code = "E_BUNDLE_PATH_INVALID";
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
return taskId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function bundleDirectory(taskId) {
|
|
21
|
+
return `${BUNDLE_ROOT}/${safeTaskId(taskId)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function copyJson(target, sourcePath, destinationPath, schemaName, packageRoot, artifacts, relativeName) {
|
|
25
|
+
try {
|
|
26
|
+
const value = await readJsonArtifact(target, sourcePath, schemaName, packageRoot);
|
|
27
|
+
await writeJsonArtifact(target, destinationPath, value.value, schemaName, packageRoot);
|
|
28
|
+
artifacts.push(relativeName);
|
|
29
|
+
return value;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error.code === "ARTIFACT_MISSING") return null;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function exportTaskBundle(target, taskId, packageRoot) {
|
|
37
|
+
safeTaskId(taskId);
|
|
38
|
+
const directory = bundleDirectory(taskId);
|
|
39
|
+
const artifacts = [];
|
|
40
|
+
const required = [
|
|
41
|
+
[ARTIFACT_PATHS.contract, "contract.json", "current-contract"],
|
|
42
|
+
[ARTIFACT_PATHS.route, "route.json", "routing-result"],
|
|
43
|
+
[ARTIFACT_PATHS.state, "state.json", "work-state"],
|
|
44
|
+
];
|
|
45
|
+
for (const [sourcePath, destinationName, schemaName] of required) {
|
|
46
|
+
const source = schemaName === "current-contract"
|
|
47
|
+
? await readContract(target, packageRoot)
|
|
48
|
+
: await readJsonArtifact(target, sourcePath, schemaName, packageRoot);
|
|
49
|
+
if (source.value.taskId !== undefined && source.value.taskId !== taskId) {
|
|
50
|
+
const error = new Error(`${sourcePath} belongs to ${source.value.taskId}, not ${taskId}`);
|
|
51
|
+
error.code = "E_BUNDLE_TASK_MISMATCH";
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
await writeJsonArtifact(target, `${directory}/${destinationName}`, source.value, schemaName, packageRoot);
|
|
55
|
+
artifacts.push(destinationName);
|
|
56
|
+
}
|
|
57
|
+
const optional = [
|
|
58
|
+
[ARTIFACT_PATHS.preflight, "preflight.json", "preflight"],
|
|
59
|
+
[ARTIFACT_PATHS.receipt, "receipt.json", "execution-receipt"],
|
|
60
|
+
[ARTIFACT_PATHS.sources, "sources.json", "source-registry"],
|
|
61
|
+
[ARTIFACT_PATHS.config, "config.json", "config"],
|
|
62
|
+
];
|
|
63
|
+
for (const [sourcePath, destinationName, schemaName] of optional) {
|
|
64
|
+
const copied = await copyJson(target, sourcePath, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
|
|
65
|
+
if (copied && !artifacts.includes(destinationName)) artifacts.push(destinationName);
|
|
66
|
+
}
|
|
67
|
+
const eventsPath = ensureWithin(target, ARTIFACT_PATHS.events);
|
|
68
|
+
if (await fileExists(eventsPath)) {
|
|
69
|
+
await assertSafePath(target, `${directory}/events.ndjson`);
|
|
70
|
+
await writeFileAtomic(ensureWithin(target, `${directory}/events.ndjson`), await readBytes(eventsPath));
|
|
71
|
+
artifacts.push("events.ndjson");
|
|
72
|
+
}
|
|
73
|
+
const gateDirectory = ensureWithin(target, ARTIFACT_PATHS.gates);
|
|
74
|
+
if (await fileExists(gateDirectory)) {
|
|
75
|
+
const entries = await readdir(gateDirectory, { withFileTypes: true });
|
|
76
|
+
for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
77
|
+
const gateName = entry.name.slice(0, -5);
|
|
78
|
+
const sourcePath = `${ARTIFACT_PATHS.gates}/${entry.name}`;
|
|
79
|
+
const destinationPath = `${directory}/gates/${entry.name}`;
|
|
80
|
+
const gate = await readJsonArtifact(target, sourcePath, "gate", packageRoot);
|
|
81
|
+
if (gate.value.taskId !== taskId) continue;
|
|
82
|
+
await writeJsonArtifact(target, destinationPath, gate.value, "gate", packageRoot);
|
|
83
|
+
artifacts.push(`gates/${gateName}.json`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
artifacts.sort();
|
|
87
|
+
const manifest = {
|
|
88
|
+
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
89
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
90
|
+
taskId,
|
|
91
|
+
artifacts,
|
|
92
|
+
};
|
|
93
|
+
await writeJsonArtifact(target, `${directory}/bundle.json`, manifest, "task-bundle", packageRoot);
|
|
94
|
+
return { ...manifest, path: `${directory}/bundle.json` };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function readTaskBundle(target, taskId, packageRoot) {
|
|
98
|
+
const directory = bundleDirectory(taskId);
|
|
99
|
+
const manifest = await readJsonArtifact(target, `${directory}/bundle.json`, "task-bundle", packageRoot);
|
|
100
|
+
const loaded = {};
|
|
101
|
+
const mappings = {
|
|
102
|
+
"contract.json": ["contract", "current-contract"],
|
|
103
|
+
"route.json": ["route", "routing-result"],
|
|
104
|
+
"state.json": ["state", "work-state"],
|
|
105
|
+
"preflight.json": ["preflight", "preflight"],
|
|
106
|
+
"receipt.json": ["receipt", "execution-receipt"],
|
|
107
|
+
"sources.json": ["sources", "source-registry"],
|
|
108
|
+
"config.json": ["config", "config"],
|
|
109
|
+
};
|
|
110
|
+
for (const artifact of manifest.value.artifacts) {
|
|
111
|
+
const mapping = mappings[artifact];
|
|
112
|
+
if (!mapping) continue;
|
|
113
|
+
const loadedArtifact = await readJsonArtifact(target, `${directory}/${artifact}`, mapping[1], packageRoot);
|
|
114
|
+
if (mapping[1] === "current-contract") {
|
|
115
|
+
await validateContract(loadedArtifact.value, packageRoot);
|
|
116
|
+
}
|
|
117
|
+
loaded[mapping[0]] = loadedArtifact.value;
|
|
118
|
+
}
|
|
119
|
+
return { manifest: manifest.value, artifacts: loaded };
|
|
120
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
+
|
|
3
|
+
export const CHECK_SCHEMA_VERSION = 1;
|
|
4
|
+
export const CHECK_STATUSES = Object.freeze(["passed", "failed", "blocked", "not-run"]);
|
|
5
|
+
export const CHECK_EVIDENCE_KINDS = Object.freeze(["OBSERVED", "INFERRED", "NOT_VERIFIED", "BLOCKED"]);
|
|
6
|
+
|
|
7
|
+
function checkError(code, message, artifacts = []) {
|
|
8
|
+
const error = new Error(message);
|
|
9
|
+
error.code = code;
|
|
10
|
+
error.artifacts = artifacts;
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function string(value, label) {
|
|
15
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
16
|
+
throw checkError("E_CHECK_INVALID", `${label} must be a non-empty string`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function optionalString(value, label) {
|
|
22
|
+
if (value !== undefined) string(value, label);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function contradiction(message) {
|
|
26
|
+
return checkError("E_CHECK_STATUS_CONTRADICTION", message);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createCheck(input = {}) {
|
|
30
|
+
const check = {
|
|
31
|
+
schemaVersion: CHECK_SCHEMA_VERSION,
|
|
32
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
33
|
+
id: input.id,
|
|
34
|
+
kind: input.kind,
|
|
35
|
+
requirement: input.requirement,
|
|
36
|
+
status: input.status ?? "not-run",
|
|
37
|
+
evidenceKind: input.evidenceKind ?? "NOT_VERIFIED",
|
|
38
|
+
source: input.source,
|
|
39
|
+
...(input.exitCode !== undefined ? { exitCode: input.exitCode } : {}),
|
|
40
|
+
...(input.timestamp !== undefined ? { timestamp: input.timestamp } : {}),
|
|
41
|
+
...(input.repositoryFingerprint !== undefined ? { repositoryFingerprint: input.repositoryFingerprint } : {}),
|
|
42
|
+
...(input.viewport !== undefined ? { viewport: structuredClone(input.viewport) } : {}),
|
|
43
|
+
...(input.details !== undefined ? { details: structuredClone(input.details) } : {}),
|
|
44
|
+
};
|
|
45
|
+
return assertCheck(check);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function assertCheck(value, label = "check") {
|
|
49
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
50
|
+
throw checkError("E_CHECK_INVALID", `${label} must be an object`);
|
|
51
|
+
}
|
|
52
|
+
if (value.schemaVersion !== CHECK_SCHEMA_VERSION || value.protocolVersion !== PROTOCOL_VERSION) {
|
|
53
|
+
throw checkError("E_CHECK_INVALID", `${label} has an unsupported protocol version`);
|
|
54
|
+
}
|
|
55
|
+
string(value.id, `${label}.id`);
|
|
56
|
+
string(value.kind, `${label}.kind`);
|
|
57
|
+
string(value.requirement, `${label}.requirement`);
|
|
58
|
+
string(value.source, `${label}.source`);
|
|
59
|
+
if (!CHECK_STATUSES.includes(value.status)) {
|
|
60
|
+
throw checkError("E_CHECK_INVALID", `${label}.status must be one of ${CHECK_STATUSES.join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
if (!CHECK_EVIDENCE_KINDS.includes(value.evidenceKind)) {
|
|
63
|
+
throw checkError("E_EVIDENCE_KIND_INVALID", `${label}.evidenceKind must be one of ${CHECK_EVIDENCE_KINDS.join(", ")}`);
|
|
64
|
+
}
|
|
65
|
+
if (value.exitCode !== undefined && (!Number.isInteger(value.exitCode) || value.exitCode < 0)) {
|
|
66
|
+
throw checkError("E_CHECK_INVALID", `${label}.exitCode must be a non-negative integer`);
|
|
67
|
+
}
|
|
68
|
+
optionalString(value.timestamp, `${label}.timestamp`);
|
|
69
|
+
if (value.status === "passed" && ["NOT_VERIFIED", "BLOCKED"].includes(value.evidenceKind)) {
|
|
70
|
+
throw contradiction(`${label} passed cannot use ${value.evidenceKind} evidence`);
|
|
71
|
+
}
|
|
72
|
+
if (value.status === "passed" && value.exitCode !== undefined && value.exitCode !== 0) {
|
|
73
|
+
throw contradiction(`${label} passed requires exitCode 0 when exitCode is recorded`);
|
|
74
|
+
}
|
|
75
|
+
if (value.status === "blocked" && value.evidenceKind !== "BLOCKED") {
|
|
76
|
+
throw contradiction(`${label} blocked must use BLOCKED evidence`);
|
|
77
|
+
}
|
|
78
|
+
if (value.status === "not-run" && value.evidenceKind !== "NOT_VERIFIED") {
|
|
79
|
+
throw contradiction(`${label} not-run must use NOT_VERIFIED evidence`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function assertCheckList(value, label = "checks") {
|
|
85
|
+
if (!Array.isArray(value)) throw checkError("E_CHECK_INVALID", `${label} must be an array`);
|
|
86
|
+
const ids = new Set();
|
|
87
|
+
value.forEach((item, index) => {
|
|
88
|
+
assertCheck(item, `${label}[${index}]`);
|
|
89
|
+
if (ids.has(item.id)) throw checkError("E_CHECK_INVALID", `${label} contains duplicate id ${item.id}`);
|
|
90
|
+
ids.add(item.id);
|
|
91
|
+
});
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function requiredChecksSatisfied(checks, requiredIds, { allowInferred = false } = {}) {
|
|
96
|
+
assertCheckList(checks);
|
|
97
|
+
if (!Array.isArray(requiredIds)) throw checkError("E_CHECK_INVALID", "requiredIds must be an array");
|
|
98
|
+
const byId = new Map(checks.map((check) => [check.id, check]));
|
|
99
|
+
const errors = [];
|
|
100
|
+
for (const id of requiredIds) {
|
|
101
|
+
const check = byId.get(id);
|
|
102
|
+
if (!check) {
|
|
103
|
+
errors.push(checkError("E_EVIDENCE_REQUIRED", `Required check is missing: ${id}`, [id]));
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (check.status !== "passed") {
|
|
107
|
+
errors.push(checkError("E_EVIDENCE_REQUIRED", `Required check is not passed: ${id}`, [id]));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!allowInferred && check.evidenceKind !== "OBSERVED") {
|
|
111
|
+
errors.push(checkError("E_EVIDENCE_KIND_INVALID", `Required check must be observed: ${id}`, [id]));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return errors;
|
|
115
|
+
}
|