@cassiomc1/forgeloop 0.1.1 → 0.1.3
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 +12 -1
- package/.github/copilot-instructions.md +11 -0
- package/AGENTS.md +13 -1
- package/AGENT_COMPATIBILITY.md +14 -1
- package/CLAUDE.md +11 -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 +163 -4
- package/ORCHESTRATOR_INTEGRATION.md +15 -0
- package/QUALITY_SCORECARD.md +16 -0
- package/README.md +64 -1
- package/THREAT_MODEL.md +6 -0
- package/conformance/README.md +47 -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 +4 -3
- 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 +249 -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/next.js +29 -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 +321 -0
- package/src/core/completion-relationships.js +141 -0
- package/src/core/completion.js +266 -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 +158 -0
- package/src/core/execution-prerequisites.js +200 -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/next-action.js +709 -0
- package/src/core/phase.js +212 -0
- package/src/core/policies.js +69 -0
- package/src/core/preflight.js +409 -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 +11 -0
- package/src/core/work-state.js +41 -13
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
2
|
+
import { requiredEvidenceForTarget } from "./completion-artifacts.js";
|
|
3
|
+
import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger } from "./events.js";
|
|
4
|
+
import { evaluatePreflight } from "./preflight.js";
|
|
5
|
+
import { readContract } from "./contract.js";
|
|
6
|
+
import { readPersistedRoute } from "./route-artifact.js";
|
|
7
|
+
import { readWorkState, writeWorkState, classifyLoadedWorkState } from "./work-state.js";
|
|
8
|
+
import { createReceipt, validateReceipt } from "./receipt.js";
|
|
9
|
+
import { completionRelationshipErrors } from "./completion-relationships.js";
|
|
10
|
+
import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
|
|
11
|
+
import { evaluateStartExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
|
|
12
|
+
|
|
13
|
+
function issue(code, message, artifacts = [], details = {}) {
|
|
14
|
+
return { code, message, artifacts, ...details };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function completionIdentityErrors({ contract, state, receipt } = {}) {
|
|
18
|
+
return completionRelationshipErrors({ contract, state, receipt })
|
|
19
|
+
.filter((error) => ["E_STATE_TASK_MISMATCH", "E_RECEIPT_TASK_MISMATCH"].includes(error.code));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function repairNext(error) {
|
|
23
|
+
switch (error.code) {
|
|
24
|
+
case "E_RECEIPT_MISSING":
|
|
25
|
+
return "Run forgeloop prepare-completion after recording verification evidence.";
|
|
26
|
+
case "E_RECEIPT_INVALID":
|
|
27
|
+
case "E_RECEIPT_CONTRACT_MISMATCH":
|
|
28
|
+
case "E_RECEIPT_ROUTE_MISMATCH":
|
|
29
|
+
case "E_RECEIPT_STATE_MISMATCH":
|
|
30
|
+
return "Run forgeloop prepare-completion, inspect the receipt, and validate it before retrying completion.";
|
|
31
|
+
case "E_PHASE_PREREQUISITE_MISSING":
|
|
32
|
+
return "Advance from EXECUTING through VERIFYING and REVIEWING, resolving the named prerequisite before completion.";
|
|
33
|
+
case "E_PHASE_CHRONOLOGY_INVALID":
|
|
34
|
+
return "Record the missing lifecycle event through the normal phase transition or forgeloop record-check path.";
|
|
35
|
+
case "E_EVIDENCE_REQUIRED":
|
|
36
|
+
case "E_EVIDENCE_COVERAGE_INVALID":
|
|
37
|
+
case "E_EVIDENCE_COVERAGE_PARTIAL":
|
|
38
|
+
case "E_EVIDENCE_KIND_INVALID":
|
|
39
|
+
case "E_CHECK_INVALID":
|
|
40
|
+
case "E_CHECK_STATUS_CONTRADICTION":
|
|
41
|
+
return "Run forgeloop record-check with compatible observed evidence for the named requirement.";
|
|
42
|
+
case "E_GATE_UNVERIFIED":
|
|
43
|
+
case "E_GATE_STALE":
|
|
44
|
+
return "Satisfy or refresh the named gate, then rerun forgeloop preflight.";
|
|
45
|
+
case "E_PROFILE_UNVERIFIED":
|
|
46
|
+
return "Use Standard mode for a fresh target, or verify PROJECT_PROFILE.md before Strict completion.";
|
|
47
|
+
default:
|
|
48
|
+
return "Resolve this validator finding in the named artifact before retrying completion.";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function withRepairGuidance(error) {
|
|
53
|
+
const normalized = {
|
|
54
|
+
...error,
|
|
55
|
+
code: error.code ?? "E_COMPLETION_REJECTED",
|
|
56
|
+
message: error.message ?? String(error),
|
|
57
|
+
artifacts: error.artifacts ?? [],
|
|
58
|
+
};
|
|
59
|
+
if (!normalized.next) normalized.next = repairNext(normalized);
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sortIssues(errors) {
|
|
64
|
+
const unique = [...new Map(errors.map((rawError) => {
|
|
65
|
+
const error = withRepairGuidance(rawError);
|
|
66
|
+
return [
|
|
67
|
+
`${error.code}\0${(error.artifacts ?? []).join("\0")}\0${error.message}`,
|
|
68
|
+
error,
|
|
69
|
+
];
|
|
70
|
+
})).values()];
|
|
71
|
+
return unique.sort((left, right) => left.code.localeCompare(right.code)
|
|
72
|
+
|| left.artifacts.join("\0").localeCompare(right.artifacts.join("\0"))
|
|
73
|
+
|| left.message.localeCompare(right.message));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function loadRequired(loader, code, message, artifacts, errors) {
|
|
77
|
+
try {
|
|
78
|
+
return await loader();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
errors.push(issue(error.code === "ARTIFACT_MISSING" ? code : `${code.replace(/_MISSING$/, "")}_INVALID`, `${message}: ${error.message}`, artifacts));
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function publicationStatus(receipt) {
|
|
86
|
+
if (receipt.publicationStatus) return receipt.publicationStatus;
|
|
87
|
+
if (receipt.publication?.deployed) return "deployed";
|
|
88
|
+
if (receipt.publication?.pushed) return "pushed";
|
|
89
|
+
if (receipt.publication?.committed) return "committed";
|
|
90
|
+
return receipt.changedPaths?.length > 0 ? "local-only" : "not-published";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function validateLedger(target, taskId, state, errors, packageRoot) {
|
|
94
|
+
await assertSafePath(target, ARTIFACT_PATHS.events);
|
|
95
|
+
const eventsPath = ensureWithin(target, ARTIFACT_PATHS.events);
|
|
96
|
+
if (!(await fileExists(eventsPath))) {
|
|
97
|
+
errors.push(issue("E_PHASE_CHRONOLOGY_INVALID", "Protocol event ledger is required before completion", [ARTIFACT_PATHS.events]));
|
|
98
|
+
return { valid: false, events: [], errors: [] };
|
|
99
|
+
}
|
|
100
|
+
const ledger = await validateEventLedger(target, packageRoot);
|
|
101
|
+
for (const error of ledger.errors) errors.push({ ...error, artifacts: [ARTIFACT_PATHS.events] });
|
|
102
|
+
const requiredEvents = LIFECYCLE_MILESTONES.slice(0, state?.phase === "COMPLETE" ? undefined : -1);
|
|
103
|
+
const observed = new Set(ledger.events.filter((event) => event.taskId === taskId).map((event) => event.event));
|
|
104
|
+
for (const event of requiredEvents) {
|
|
105
|
+
if (!observed.has(event)) {
|
|
106
|
+
errors.push(issue("E_PHASE_CHRONOLOGY_INVALID", `Required protocol event is missing: ${event}`, [ARTIFACT_PATHS.events], { event }));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (ledger.events.some((event) => event.taskId !== taskId)) {
|
|
110
|
+
errors.push(issue("E_PHASE_CHRONOLOGY_INVALID", "Protocol events must belong to the current task", [ARTIFACT_PATHS.events]));
|
|
111
|
+
}
|
|
112
|
+
if (state?.phase !== "COMPLETE" && observed.has("COMPLETION_VALIDATED")) {
|
|
113
|
+
errors.push(issue("E_PHASE_CHRONOLOGY_INVALID", "COMPLETION_VALIDATED requires COMPLETE state", [ARTIFACT_PATHS.events]));
|
|
114
|
+
}
|
|
115
|
+
return ledger;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function evaluateCompletion({ target, packageRoot, strict = false } = {}) {
|
|
119
|
+
const errors = [];
|
|
120
|
+
const preflight = await evaluatePreflight({ target, packageRoot, strict });
|
|
121
|
+
errors.push(...preflight.errors);
|
|
122
|
+
const contract = await loadRequired(
|
|
123
|
+
() => readContract(target, packageRoot),
|
|
124
|
+
"E_CONTRACT_MISSING",
|
|
125
|
+
"Current contract is not available",
|
|
126
|
+
[ARTIFACT_PATHS.contract],
|
|
127
|
+
errors,
|
|
128
|
+
);
|
|
129
|
+
const route = await loadRequired(
|
|
130
|
+
() => readPersistedRoute(target, packageRoot),
|
|
131
|
+
"E_ROUTE_MISSING",
|
|
132
|
+
"Persisted routing result is not available",
|
|
133
|
+
[ARTIFACT_PATHS.route],
|
|
134
|
+
errors,
|
|
135
|
+
);
|
|
136
|
+
const state = await loadRequired(
|
|
137
|
+
() => readWorkState(target, packageRoot).then((value) => {
|
|
138
|
+
if (!value) {
|
|
139
|
+
const error = new Error("Work state is missing");
|
|
140
|
+
error.code = "ARTIFACT_MISSING";
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}),
|
|
145
|
+
"E_STATE_MISSING",
|
|
146
|
+
"Work state is not available",
|
|
147
|
+
[ARTIFACT_PATHS.state],
|
|
148
|
+
errors,
|
|
149
|
+
);
|
|
150
|
+
const receipt = await loadRequired(
|
|
151
|
+
() => readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot),
|
|
152
|
+
"E_RECEIPT_MISSING",
|
|
153
|
+
"Execution receipt is not available",
|
|
154
|
+
[ARTIFACT_PATHS.receipt],
|
|
155
|
+
errors,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (state && hasExecutionStarted(state.phase)) {
|
|
159
|
+
const prerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot });
|
|
160
|
+
errors.push(...prerequisites.errors);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (receipt) {
|
|
164
|
+
try {
|
|
165
|
+
await validateReceipt(receipt.value, packageRoot);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
errors.push(issue(error.code ?? "E_RECEIPT_INVALID", `Execution receipt is invalid: ${error.message}`, [ARTIFACT_PATHS.receipt]));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (state && !["REVIEWING", "COMPLETE"].includes(state.phase)) {
|
|
172
|
+
errors.push(issue("E_PHASE_PREREQUISITE_MISSING", `Completion requires REVIEWING or COMPLETE state, found ${state.phase}`, [ARTIFACT_PATHS.state]));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let coverage = [];
|
|
176
|
+
if (contract && route) {
|
|
177
|
+
const requiredEvidence = await requiredEvidenceForTarget({
|
|
178
|
+
target,
|
|
179
|
+
contract,
|
|
180
|
+
route,
|
|
181
|
+
packageRoot,
|
|
182
|
+
additionalEvidence: preflight?.policy?.requiredEvidence ?? [],
|
|
183
|
+
});
|
|
184
|
+
const relationshipErrors = completionRelationshipErrors({
|
|
185
|
+
contract,
|
|
186
|
+
route,
|
|
187
|
+
state,
|
|
188
|
+
receipt: receipt?.value,
|
|
189
|
+
requiredEvidence,
|
|
190
|
+
});
|
|
191
|
+
errors.push(...relationshipErrors);
|
|
192
|
+
coverage = receipt?.value?.evidenceCoverage ?? [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const ledger = contract && state
|
|
196
|
+
? await validateLedger(target, contract.value.taskId, state, errors, packageRoot)
|
|
197
|
+
: { valid: false, events: [], errors: [] };
|
|
198
|
+
|
|
199
|
+
if (state) {
|
|
200
|
+
try {
|
|
201
|
+
const classification = await classifyLoadedWorkState({
|
|
202
|
+
target,
|
|
203
|
+
state,
|
|
204
|
+
contractFile: ARTIFACT_PATHS.contract,
|
|
205
|
+
});
|
|
206
|
+
if (classification.status === "REVALIDATION_REQUIRED") {
|
|
207
|
+
for (const reason of classification.reasons) {
|
|
208
|
+
errors.push(issue(reason === "CONTRACT_CHANGED" ? "E_CONTRACT_STALE" : "E_PHASE_ARTIFACT_STALE", `State freshness check failed: ${reason}`, [ARTIFACT_PATHS.state]));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
errors.push(issue("E_PHASE_ARTIFACT_STALE", error.message, [ARTIFACT_PATHS.state]));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const sortedErrors = sortIssues(errors);
|
|
217
|
+
const receiptValue = receipt?.value;
|
|
218
|
+
const publication = receiptValue ? publicationStatus(receiptValue) : "not-published";
|
|
219
|
+
const valid = sortedErrors.length === 0;
|
|
220
|
+
return {
|
|
221
|
+
status: valid ? "VALID" : "REJECTED",
|
|
222
|
+
taskStatus: valid ? "COMPLETE" : receiptValue?.status === "blocked" ? "BLOCKED" : "INCOMPLETE",
|
|
223
|
+
verificationStatus: valid ? "VALID" : "invalid",
|
|
224
|
+
publicationStatus: publication,
|
|
225
|
+
productionReadiness: receiptValue?.productionReadiness ?? "not-verified",
|
|
226
|
+
errors: sortedErrors,
|
|
227
|
+
warnings: [],
|
|
228
|
+
preflight,
|
|
229
|
+
coverage,
|
|
230
|
+
ledger: {
|
|
231
|
+
status: ledger.valid ? "valid" : "invalid",
|
|
232
|
+
events: ledger.events.length,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function runComplete({ target, packageRoot, strict = false, persist = true } = {}) {
|
|
238
|
+
const result = await evaluateCompletion({ target, packageRoot, strict });
|
|
239
|
+
if (persist && result.status === "VALID") {
|
|
240
|
+
const state = await readWorkState(target, packageRoot);
|
|
241
|
+
if (state && state.phase !== "COMPLETE") {
|
|
242
|
+
const receipt = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
|
|
243
|
+
const next = {
|
|
244
|
+
...state,
|
|
245
|
+
previousPhase: state.phase,
|
|
246
|
+
phase: "COMPLETE",
|
|
247
|
+
evidenceCoverage: result.coverage,
|
|
248
|
+
verificationEvidence: receipt.value.evidence,
|
|
249
|
+
publicationStatus: result.publicationStatus,
|
|
250
|
+
lastUpdated: new Date().toISOString(),
|
|
251
|
+
};
|
|
252
|
+
const nextReceipt = await createReceipt({
|
|
253
|
+
...receipt.value,
|
|
254
|
+
stateFingerprint: canonicalFingerprint(next),
|
|
255
|
+
}, packageRoot);
|
|
256
|
+
await writeWorkState(target, next, { packageRoot });
|
|
257
|
+
await writeJsonArtifact(target, ARTIFACT_PATHS.receipt, nextReceipt, "execution-receipt", packageRoot);
|
|
258
|
+
}
|
|
259
|
+
const contract = await readContract(target, packageRoot);
|
|
260
|
+
const ledger = await validateEventLedger(target, packageRoot);
|
|
261
|
+
if (!ledger.events.some((event) => event.event === "COMPLETION_VALIDATED")) {
|
|
262
|
+
await appendProtocolEvent(target, { taskId: contract.value.taskId, event: "COMPLETION_VALIDATED" }, packageRoot);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
+
import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
3
|
+
|
|
4
|
+
export const CONFIG_SCHEMA_VERSION = 1;
|
|
5
|
+
export const COMPLIANCE_MODES = Object.freeze(["advisory", "standard", "strict"]);
|
|
6
|
+
|
|
7
|
+
function stringArray(value, label) {
|
|
8
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
9
|
+
throw new Error(`${label} must be an array of non-empty strings`);
|
|
10
|
+
}
|
|
11
|
+
return [...new Set(value)];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createConfig(input = {}) {
|
|
15
|
+
const complianceMode = input.complianceMode ?? "standard";
|
|
16
|
+
if (!COMPLIANCE_MODES.includes(complianceMode)) {
|
|
17
|
+
throw new Error(`Unknown compliance mode: ${complianceMode}`);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
21
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
22
|
+
complianceMode,
|
|
23
|
+
...(input.policy !== undefined ? { policy: input.policy } : {}),
|
|
24
|
+
...(input.requiredGates !== undefined ? { requiredGates: stringArray(input.requiredGates, "requiredGates") } : {}),
|
|
25
|
+
...(input.requiredEvidence !== undefined ? { requiredEvidence: stringArray(input.requiredEvidence, "requiredEvidence") } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function readConfig(target, packageRoot) {
|
|
30
|
+
return (await readJsonArtifact(target, ARTIFACT_PATHS.config, "config", packageRoot)).value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function writeConfig(target, config, packageRoot, options = {}) {
|
|
34
|
+
return writeJsonArtifact(target, ARTIFACT_PATHS.config, config, "config", packageRoot, options);
|
|
35
|
+
}
|
package/src/core/conformance.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
2
2
|
import { createEvidence } from "./evidence.js";
|
|
3
|
+
import { canonicalFingerprint } from "./artifacts.js";
|
|
3
4
|
|
|
4
5
|
function error(code, message, artifacts = []) {
|
|
5
6
|
return { code, message, artifacts };
|
|
@@ -51,6 +52,12 @@ export function validateTaskArtifactSet({
|
|
|
51
52
|
if (route && state && JSON.stringify(route.guides) !== JSON.stringify(state.selectedGuides)) {
|
|
52
53
|
errors.push(error("ROUTE_STATE_GUIDES_MISMATCH", "work-state.selectedGuides must equal routing-result.guides", ["route", "state"]));
|
|
53
54
|
}
|
|
55
|
+
if (route && state && state.routeFingerprint !== undefined && state.routeFingerprint !== canonicalFingerprint(route)) {
|
|
56
|
+
errors.push(error("ROUTE_STATE_FINGERPRINT_MISMATCH", "work-state.routeFingerprint must match routing-result", ["route", "state"]));
|
|
57
|
+
}
|
|
58
|
+
if (route && receipt && receipt.routeFingerprint !== undefined && receipt.routeFingerprint !== canonicalFingerprint(route)) {
|
|
59
|
+
errors.push(error("STATE_RECEIPT_ROUTE_MISMATCH", "execution-receipt.routeFingerprint must match routing-result", ["route", "receipt"]));
|
|
60
|
+
}
|
|
54
61
|
if (state && receipt && JSON.stringify(state.selectedGuides) !== JSON.stringify(receipt.selectedGuides)) {
|
|
55
62
|
errors.push(error("STATE_RECEIPT_GUIDES_MISMATCH", "execution-receipt.selectedGuides must equal work-state.selectedGuides", ["state", "receipt"]));
|
|
56
63
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
+
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
3
|
+
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
4
|
+
import { assertSecretFree } from "./receipt.js";
|
|
5
|
+
|
|
6
|
+
export const CONTRACT_SCHEMA_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
const CONTRACT_ARRAY_FIELDS = Object.freeze([
|
|
9
|
+
"deliverables",
|
|
10
|
+
"constraints",
|
|
11
|
+
"risks",
|
|
12
|
+
"verification",
|
|
13
|
+
"successCriteria",
|
|
14
|
+
"stopConditions",
|
|
15
|
+
"unresolvedDecisions",
|
|
16
|
+
"sourceRefs",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function assertStringArray(value, label) {
|
|
20
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
21
|
+
throw new Error(`${label} must be an array of non-empty strings`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ASSUMPTION_FIELDS = Object.freeze([
|
|
26
|
+
"value",
|
|
27
|
+
"reason",
|
|
28
|
+
"scope",
|
|
29
|
+
"reversible",
|
|
30
|
+
"source",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function assertAssumptionObject(value, label) {
|
|
34
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
35
|
+
throw new Error(`${label} must be a plain object`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const keys = Object.keys(value);
|
|
39
|
+
const unexpected = keys.filter((key) => !ASSUMPTION_FIELDS.includes(key));
|
|
40
|
+
if (unexpected.length > 0) {
|
|
41
|
+
throw new Error(`${label} contains unknown property: ${unexpected[0]}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const field of ASSUMPTION_FIELDS) {
|
|
45
|
+
if (!Object.prototype.hasOwnProperty.call(value, field)) {
|
|
46
|
+
throw new Error(`${label}.${field} is required`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof value.value !== "string" || value.value.trim() === "") {
|
|
51
|
+
throw new Error(`${label}.value must be a non-empty string`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof value.reason !== "string" || value.reason.trim() === "") {
|
|
54
|
+
throw new Error(`${label}.reason must be a non-empty string`);
|
|
55
|
+
}
|
|
56
|
+
if (typeof value.scope !== "string" || value.scope.trim() === "") {
|
|
57
|
+
throw new Error(`${label}.scope must be a non-empty string`);
|
|
58
|
+
}
|
|
59
|
+
if (value.reversible !== true) {
|
|
60
|
+
throw new Error(`${label}.reversible must be true`);
|
|
61
|
+
}
|
|
62
|
+
if (value.source !== "agent-default") {
|
|
63
|
+
throw new Error(`${label}.source must be agent-default`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function assertAssumptions(value, label = "Contract assumptions") {
|
|
68
|
+
if (!Array.isArray(value)) {
|
|
69
|
+
throw new Error(`${label} must be an array`);
|
|
70
|
+
}
|
|
71
|
+
value.forEach((item, index) => {
|
|
72
|
+
assertAssumptionObject(item, `${label}[${index}]`);
|
|
73
|
+
});
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createContract(input = {}) {
|
|
78
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
79
|
+
throw new Error("Contract must be a JSON object");
|
|
80
|
+
}
|
|
81
|
+
if (typeof input.taskId !== "string" || input.taskId.trim() === "") {
|
|
82
|
+
throw new Error("Contract taskId is required");
|
|
83
|
+
}
|
|
84
|
+
if (typeof input.objective !== "string" || input.objective.trim() === "") {
|
|
85
|
+
throw new Error("Contract objective is required");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const contract = {
|
|
89
|
+
schemaVersion: CONTRACT_SCHEMA_VERSION,
|
|
90
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
91
|
+
taskId: input.taskId,
|
|
92
|
+
objective: input.objective,
|
|
93
|
+
assumptions: [],
|
|
94
|
+
};
|
|
95
|
+
assertAssumptions(input.assumptions ?? []);
|
|
96
|
+
for (const field of CONTRACT_ARRAY_FIELDS) {
|
|
97
|
+
const value = input[field] ?? [];
|
|
98
|
+
assertStringArray(value, `Contract ${field}`);
|
|
99
|
+
contract[field] = [...value];
|
|
100
|
+
}
|
|
101
|
+
contract.assumptions = (input.assumptions ?? []).map((assumption) => ({
|
|
102
|
+
value: assumption.value,
|
|
103
|
+
reason: assumption.reason,
|
|
104
|
+
scope: assumption.scope,
|
|
105
|
+
reversible: assumption.reversible,
|
|
106
|
+
source: assumption.source,
|
|
107
|
+
}));
|
|
108
|
+
assertSecretFree(contract);
|
|
109
|
+
return contract;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function contractFingerprint(contract) {
|
|
113
|
+
return canonicalFingerprint(contract);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function validateContract(contract, packageRoot) {
|
|
117
|
+
assertSecretFree(contract);
|
|
118
|
+
assertAssumptions(contract.assumptions ?? []);
|
|
119
|
+
const schema = await readSchema("current-contract", packageRoot);
|
|
120
|
+
assertSchema(contract, schema, "current contract");
|
|
121
|
+
return contract;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function readContract(target, packageRoot) {
|
|
125
|
+
const artifact = await readJsonArtifact(target, ARTIFACT_PATHS.contract, "current-contract", packageRoot);
|
|
126
|
+
await validateContract(artifact.value, packageRoot);
|
|
127
|
+
return artifact;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function writeContract(target, contract, packageRoot, options = {}) {
|
|
131
|
+
assertSecretFree(contract);
|
|
132
|
+
assertAssumptions(contract.assumptions ?? []);
|
|
133
|
+
return writeJsonArtifact(
|
|
134
|
+
target,
|
|
135
|
+
ARTIFACT_PATHS.contract,
|
|
136
|
+
contract,
|
|
137
|
+
"current-contract",
|
|
138
|
+
packageRoot,
|
|
139
|
+
options,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
+
|
|
3
|
+
export const COVERAGE_SCHEMA_VERSION = 1;
|
|
4
|
+
export const COVERAGE_STATUSES = Object.freeze(["COVERED", "PARTIAL", "NOT_VERIFIED", "BLOCKED"]);
|
|
5
|
+
|
|
6
|
+
function coverageError(code, message) {
|
|
7
|
+
const error = new Error(message);
|
|
8
|
+
error.code = code;
|
|
9
|
+
return error;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function stringArray(value, label) {
|
|
13
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
14
|
+
throw coverageError("E_EVIDENCE_COVERAGE_INVALID", `${label} must be an array of non-empty strings`);
|
|
15
|
+
}
|
|
16
|
+
return [...new Set(value)];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function evaluateCoverage(requiredEvidence, observedEvidence, { blocked = false } = {}) {
|
|
20
|
+
const required = stringArray(requiredEvidence, "requiredEvidence");
|
|
21
|
+
const observed = stringArray(observedEvidence, "observedEvidence");
|
|
22
|
+
if (blocked) return "BLOCKED";
|
|
23
|
+
if (required.length === 0 || required.every((item) => observed.includes(item))) return "COVERED";
|
|
24
|
+
if (required.some((item) => observed.includes(item))) return "PARTIAL";
|
|
25
|
+
return "NOT_VERIFIED";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createCoverage(input = {}) {
|
|
29
|
+
const requiredEvidence = stringArray(input.requiredEvidence ?? [], "requiredEvidence");
|
|
30
|
+
const observedEvidence = stringArray(input.observedEvidence ?? [], "observedEvidence");
|
|
31
|
+
const status = input.status ?? evaluateCoverage(requiredEvidence, observedEvidence, { blocked: input.blocked === true });
|
|
32
|
+
const value = {
|
|
33
|
+
schemaVersion: COVERAGE_SCHEMA_VERSION,
|
|
34
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
35
|
+
requirement: input.requirement,
|
|
36
|
+
requiredEvidence,
|
|
37
|
+
observedEvidence,
|
|
38
|
+
status,
|
|
39
|
+
...(input.details !== undefined ? { details: structuredClone(input.details) } : {}),
|
|
40
|
+
};
|
|
41
|
+
return assertCoverage(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function assertCoverage(value, label = "evidence coverage") {
|
|
45
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
46
|
+
throw coverageError("E_EVIDENCE_COVERAGE_INVALID", `${label} must be an object`);
|
|
47
|
+
}
|
|
48
|
+
if (value.schemaVersion !== COVERAGE_SCHEMA_VERSION || value.protocolVersion !== PROTOCOL_VERSION) {
|
|
49
|
+
throw coverageError("E_EVIDENCE_COVERAGE_INVALID", `${label} has an unsupported protocol version`);
|
|
50
|
+
}
|
|
51
|
+
if (typeof value.requirement !== "string" || value.requirement.trim() === "") {
|
|
52
|
+
throw coverageError("E_EVIDENCE_COVERAGE_INVALID", `${label}.requirement is required`);
|
|
53
|
+
}
|
|
54
|
+
const requiredEvidence = stringArray(value.requiredEvidence, `${label}.requiredEvidence`);
|
|
55
|
+
const observedEvidence = stringArray(value.observedEvidence, `${label}.observedEvidence`);
|
|
56
|
+
if (!COVERAGE_STATUSES.includes(value.status)) {
|
|
57
|
+
throw coverageError("E_EVIDENCE_COVERAGE_INVALID", `${label}.status is invalid`);
|
|
58
|
+
}
|
|
59
|
+
const expected = evaluateCoverage(requiredEvidence, observedEvidence, {
|
|
60
|
+
blocked: value.status === "BLOCKED",
|
|
61
|
+
});
|
|
62
|
+
if (value.status !== expected && !(value.status === "BLOCKED" && expected === "BLOCKED")) {
|
|
63
|
+
throw coverageError("E_EVIDENCE_COVERAGE_PARTIAL", `${label}.status does not match its observed evidence`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function assertCoverageList(value, label = "evidenceCoverage") {
|
|
69
|
+
if (!Array.isArray(value)) throw coverageError("E_EVIDENCE_COVERAGE_INVALID", `${label} must be an array`);
|
|
70
|
+
value.forEach((item, index) => assertCoverage(item, `${label}[${index}]`));
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function coverageForRequirements(requirements, checks, { blockedIds = [] } = {}) {
|
|
75
|
+
const normalizedRequirements = stringArray(requirements ?? [], "requirements");
|
|
76
|
+
const normalizedChecks = Array.isArray(checks) ? checks : [];
|
|
77
|
+
const observed = new Set(
|
|
78
|
+
normalizedChecks
|
|
79
|
+
.filter((check) => check?.status === "passed" && check.evidenceKind === "OBSERVED")
|
|
80
|
+
.flatMap((check) => [check.id, check.kind, check.requirement]),
|
|
81
|
+
);
|
|
82
|
+
const blocked = new Set(blockedIds);
|
|
83
|
+
return normalizedRequirements.map((requirement) => createCoverage({
|
|
84
|
+
requirement,
|
|
85
|
+
requiredEvidence: [requirement],
|
|
86
|
+
observedEvidence: observed.has(requirement) ? [requirement] : [],
|
|
87
|
+
blocked: blocked.has(requirement),
|
|
88
|
+
}));
|
|
89
|
+
}
|