@cassiomc1/forgeloop 0.1.10 → 0.1.12
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 +16 -18
- package/.github/copilot-instructions.md +16 -19
- package/AGENTS.md +18 -24
- package/AGENT_COMPATIBILITY.md +4 -211
- package/CLAUDE.md +16 -18
- package/LOOP_ENGINEERING.md +73 -0
- package/LOOP_SYSTEM_DESIGN.md +9 -5
- package/ORCHESTRATOR_INTEGRATION.md +4 -0
- package/PROTOCOL_INTEGRATION.md +171 -0
- package/QUALITY_SCORECARD.md +1 -1
- package/README.md +123 -103
- package/TERMINOLOGY.md +2 -0
- package/THREAT_MODEL.md +4 -0
- package/package.json +5 -4
- package/schemas/current-contract.schema.json +48 -2
- package/schemas/evidence.schema.json +1 -0
- package/schemas/execution-receipt.schema.json +2 -0
- package/schemas/work-state.schema.json +44 -1
- package/src/cli.js +48 -4
- package/src/commands/record-terminal-result.js +18 -0
- package/src/commands/validate-protocol.js +14 -2
- package/src/core/checks.js +20 -0
- package/src/core/completion-artifacts.js +266 -11
- package/src/core/completion-recovery.js +15 -0
- package/src/core/completion-relationships.js +23 -9
- package/src/core/completion.js +130 -2
- package/src/core/contract.js +82 -4
- package/src/core/coverage.js +17 -12
- package/src/core/discovery-surfaces.js +22 -0
- package/src/core/events.js +183 -2
- package/src/core/evidence-readiness.js +333 -0
- package/src/core/evidence.js +22 -4
- package/src/core/execution-prerequisites.js +6 -1
- package/src/core/inspect.js +23 -12
- package/src/core/native-adapters.js +30 -2
- package/src/core/next-action.js +103 -27
- package/src/core/phase.js +68 -5
- package/src/core/protocol.js +28 -1
- package/src/core/receipt.js +5 -0
- package/src/core/schema-validation.js +28 -6
- package/src/core/templates.js +1 -0
- package/src/core/work-state.js +27 -0
- package/conformance/README.md +0 -148
- package/conformance/backend-auth/EXPECTED_ROUTE.json +0 -7
- package/conformance/backend-auth/REQUEST.md +0 -4
- package/conformance/backend-auth/REQUIRED_EVIDENCE.json +0 -3
- package/conformance/backend-auth/REQUIRED_GATES.json +0 -3
- package/conformance/blind-premium-website/EXPECTED_ROUTE.json +0 -7
- package/conformance/blind-premium-website/REQUEST.md +0 -6
- package/conformance/blind-premium-website/REQUIRED_EVIDENCE.json +0 -14
- package/conformance/blind-premium-website/REQUIRED_GATES.json +0 -3
- package/conformance/complete-website/EXPECTED_ROUTE.json +0 -7
- package/conformance/complete-website/REQUEST.md +0 -6
- package/conformance/complete-website/REQUIRED_EVIDENCE.json +0 -3
- package/conformance/complete-website/REQUIRED_GATES.json +0 -3
- package/conformance/docs-only/EXPECTED_ROUTE.json +0 -7
- package/conformance/docs-only/REQUEST.md +0 -4
- package/conformance/docs-only/REQUIRED_EVIDENCE.json +0 -3
- package/conformance/docs-only/REQUIRED_GATES.json +0 -3
- package/conformance/runs/2026-08-11-codex-first-live.md +0 -98
- package/conformance/runs/2026-08-11-codex-second-live.md +0 -87
- package/conformance/runs/2026-08-13-codex-fifth-live.md +0 -386
- package/conformance/runs/2026-08-13-codex-fourth-live.md +0 -309
- package/conformance/runs/2026-08-13-codex-sixth-live.md +0 -412
- package/conformance/simple-bug/EXPECTED_ROUTE.json +0 -7
- package/conformance/simple-bug/REQUEST.md +0 -4
- package/conformance/simple-bug/REQUIRED_EVIDENCE.json +0 -3
- package/conformance/simple-bug/REQUIRED_GATES.json +0 -3
- package/src/core/agent-support.js +0 -89
package/src/core/next-action.js
CHANGED
|
@@ -11,10 +11,12 @@ import { assertCheckList } from "./checks.js";
|
|
|
11
11
|
import { completionRelationshipErrors } from "./completion-relationships.js";
|
|
12
12
|
import { classifyLoadedWorkState, readWorkState } from "./work-state.js";
|
|
13
13
|
import { sha256 } from "./manifest.js";
|
|
14
|
+
import { evaluateRequiredEvidence, authoritativeChecksForRequirements, ordinaryLeafRequirements, classifyRequirement } from "./evidence-readiness.js";
|
|
14
15
|
import {
|
|
15
16
|
evaluateStartExecutionPrerequisites,
|
|
16
17
|
PREFLIGHT_ROUTE_IDENTITY_ERROR_MESSAGE,
|
|
17
18
|
} from "./execution-prerequisites.js";
|
|
19
|
+
import { validateEventLedger, validateCompletionRecoveryAuthorization } from "./events.js";
|
|
18
20
|
|
|
19
21
|
export const NEXT_ACTIONS = Object.freeze({
|
|
20
22
|
DISCOVER: "DISCOVER",
|
|
@@ -29,6 +31,7 @@ export const NEXT_ACTIONS = Object.freeze({
|
|
|
29
31
|
DIAGNOSE: "DIAGNOSE",
|
|
30
32
|
CORRECT: "CORRECT",
|
|
31
33
|
ENTER_REVIEWING: "ENTER_REVIEWING",
|
|
34
|
+
RECORD_TERMINAL_RESULT: "RECORD_TERMINAL_RESULT",
|
|
32
35
|
PREPARE_COMPLETION: "PREPARE_COMPLETION",
|
|
33
36
|
RUN_COMPLETE: "RUN_COMPLETE",
|
|
34
37
|
RESOLVE_STALE_ROUTE: "RESOLVE_STALE_ROUTE",
|
|
@@ -99,6 +102,7 @@ function commandFor(action) {
|
|
|
99
102
|
[NEXT_ACTIONS.DIAGNOSE]: "forgeloop advance --to DIAGNOSING",
|
|
100
103
|
[NEXT_ACTIONS.CORRECT]: "forgeloop advance --to CORRECTING",
|
|
101
104
|
[NEXT_ACTIONS.ENTER_REVIEWING]: "forgeloop advance --to REVIEWING",
|
|
105
|
+
[NEXT_ACTIONS.RECORD_TERMINAL_RESULT]: "forgeloop record-terminal-result",
|
|
102
106
|
[NEXT_ACTIONS.PREPARE_COMPLETION]: "forgeloop prepare-completion --json",
|
|
103
107
|
[NEXT_ACTIONS.RUN_COMPLETE]: "forgeloop complete --json",
|
|
104
108
|
}[action];
|
|
@@ -120,6 +124,23 @@ function recordCheckCommandSpec(requirement) {
|
|
|
120
124
|
};
|
|
121
125
|
}
|
|
122
126
|
|
|
127
|
+
function recordTerminalResultCommandSpec(requirement) {
|
|
128
|
+
const reqId = requirement.id ?? requirement;
|
|
129
|
+
const type = requirement.type ?? "PUBLICATION";
|
|
130
|
+
const status = requirement.requiredPublicationStatus ?? (type === "PUBLICATION" ? "published" : "ready");
|
|
131
|
+
return {
|
|
132
|
+
commandId: "record-terminal-result",
|
|
133
|
+
executable: "forgeloop",
|
|
134
|
+
subcommand: "record-terminal-result",
|
|
135
|
+
argv: ["record-terminal-result", `--requirement=${reqId}`, `--type=${type}`, `--status=${status}`],
|
|
136
|
+
requiredInputs: [
|
|
137
|
+
{ name: "source", option: "--source=<text>" },
|
|
138
|
+
{ name: "result", option: "--result=<text>" },
|
|
139
|
+
{ name: "details", option: "--details=<json>", optional: true },
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
123
144
|
function decision(input, action, reason, requiredArtifacts = [], missingArtifacts = []) {
|
|
124
145
|
const command = commandFor(action);
|
|
125
146
|
return result({
|
|
@@ -138,10 +159,6 @@ function missingArtifact(error, fallback) {
|
|
|
138
159
|
: [];
|
|
139
160
|
}
|
|
140
161
|
|
|
141
|
-
function allCoverageCovered(coverage) {
|
|
142
|
-
return coverage.every((item) => item.status === "COVERED");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
162
|
function staleReasons(state, contract, route) {
|
|
146
163
|
const reasons = [];
|
|
147
164
|
if (state.contractFingerprint !== contract.fingerprint) {
|
|
@@ -230,7 +247,11 @@ async function requirementsAndCoverage({ target, packageRoot, contract, route, c
|
|
|
230
247
|
return { requirements, coverage: coverageForRequirements(requirements, checks) };
|
|
231
248
|
}
|
|
232
249
|
|
|
233
|
-
export async function getNextAction(
|
|
250
|
+
export async function getNextAction(targetOrOptions = {}, packageRootOption) {
|
|
251
|
+
const normalized = typeof targetOrOptions === "string"
|
|
252
|
+
? { target: targetOrOptions, packageRoot: packageRootOption }
|
|
253
|
+
: targetOrOptions;
|
|
254
|
+
const { target, packageRoot } = normalized ?? {};
|
|
234
255
|
const workState = await loadArtifact(
|
|
235
256
|
() => readWorkState(target, packageRoot),
|
|
236
257
|
ARTIFACT_PATHS.state,
|
|
@@ -516,20 +537,32 @@ export async function getNextAction({ target, packageRoot } = {}) {
|
|
|
516
537
|
requiredArtifacts,
|
|
517
538
|
});
|
|
518
539
|
}
|
|
519
|
-
const
|
|
540
|
+
const evidence = await requirementsAndCoverage({
|
|
541
|
+
target,
|
|
542
|
+
packageRoot,
|
|
543
|
+
contract,
|
|
544
|
+
route,
|
|
545
|
+
checks: state.checks,
|
|
546
|
+
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
547
|
+
});
|
|
548
|
+
const authoritative = authoritativeChecksForRequirements({
|
|
549
|
+
requirements: ordinaryLeafRequirements(evidence.requirements),
|
|
550
|
+
checks: state.checks,
|
|
551
|
+
});
|
|
552
|
+
const failed = authoritative.find(({ check }) => check?.status === "failed");
|
|
520
553
|
if (failed) {
|
|
521
554
|
return decision(
|
|
522
555
|
context,
|
|
523
556
|
NEXT_ACTIONS.DIAGNOSE,
|
|
524
|
-
artifactError("E_CHECK_FAILED", `Observed check failed: ${failed.id}`, [ARTIFACT_PATHS.state]),
|
|
557
|
+
artifactError("E_CHECK_FAILED", `Observed check failed: ${failed.check.id}`, [ARTIFACT_PATHS.state]),
|
|
525
558
|
);
|
|
526
559
|
}
|
|
527
|
-
const blocked =
|
|
560
|
+
const blocked = authoritative.find(({ check }) => check?.status === "blocked");
|
|
528
561
|
if (blocked) {
|
|
529
562
|
return decision(
|
|
530
563
|
context,
|
|
531
564
|
NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
532
|
-
artifactError("E_CHECK_BLOCKED", `Observed check is blocked: ${blocked.id}`, [ARTIFACT_PATHS.state]),
|
|
565
|
+
artifactError("E_CHECK_BLOCKED", `Observed check is blocked: ${blocked.check.id}`, [ARTIFACT_PATHS.state]),
|
|
533
566
|
);
|
|
534
567
|
}
|
|
535
568
|
const receipt = await loadArtifact(
|
|
@@ -563,14 +596,7 @@ export async function getNextAction({ target, packageRoot } = {}) {
|
|
|
563
596
|
[...requiredArtifacts, ARTIFACT_PATHS.receipt],
|
|
564
597
|
);
|
|
565
598
|
}
|
|
566
|
-
const
|
|
567
|
-
target,
|
|
568
|
-
packageRoot,
|
|
569
|
-
contract,
|
|
570
|
-
route,
|
|
571
|
-
checks: state.checks,
|
|
572
|
-
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
573
|
-
});
|
|
599
|
+
const readiness = evaluateRequiredEvidence({ requirements: evidence.requirements, checks: state.checks });
|
|
574
600
|
const receiptRelationships = completionRelationshipErrors({
|
|
575
601
|
contract,
|
|
576
602
|
route,
|
|
@@ -587,18 +613,22 @@ export async function getNextAction({ target, packageRoot } = {}) {
|
|
|
587
613
|
requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt],
|
|
588
614
|
});
|
|
589
615
|
}
|
|
590
|
-
if (
|
|
616
|
+
if (readiness.ready) {
|
|
591
617
|
return decision(context, NEXT_ACTIONS.ENTER_REVIEWING, artifactError("EVIDENCE_COVERED", "All required observed verification evidence is covered"));
|
|
592
618
|
}
|
|
593
|
-
const uncovered =
|
|
619
|
+
const uncovered = [...readiness.invalid, ...readiness.partial, ...readiness.missing];
|
|
594
620
|
return result({
|
|
595
621
|
...context,
|
|
596
622
|
nextAction: NEXT_ACTIONS.RECORD_VERIFICATION,
|
|
597
623
|
commands: ["forgeloop record-check"],
|
|
598
|
-
commandSpecs: uncovered.map((item) => recordCheckCommandSpec(item.
|
|
624
|
+
commandSpecs: uncovered.map((item) => recordCheckCommandSpec(item.text)),
|
|
599
625
|
reasons: uncovered
|
|
600
626
|
.map((item) => artifactError(
|
|
601
|
-
|
|
627
|
+
readiness.invalid.some((candidate) => candidate.id === item.id)
|
|
628
|
+
? "E_EVIDENCE_INVALID"
|
|
629
|
+
: readiness.partial.some((candidate) => candidate.id === item.id)
|
|
630
|
+
? "E_EVIDENCE_PARTIAL"
|
|
631
|
+
: "E_EVIDENCE_REQUIRED",
|
|
602
632
|
"Run the required check and record observed evidence through the structured command specification.",
|
|
603
633
|
[ARTIFACT_PATHS.state],
|
|
604
634
|
)),
|
|
@@ -641,15 +671,41 @@ export async function getNextAction({ target, packageRoot } = {}) {
|
|
|
641
671
|
checks: state.checks,
|
|
642
672
|
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
643
673
|
});
|
|
644
|
-
|
|
674
|
+
const readiness = evaluateRequiredEvidence({ requirements: evidence.requirements, checks: state.checks });
|
|
675
|
+
if (!readiness.ready) {
|
|
676
|
+
let recoveryAuthorized = false;
|
|
677
|
+
if (state.lastCompletionAttempt?.status === "REJECTED") {
|
|
678
|
+
try {
|
|
679
|
+
const ledger = await validateEventLedger(target, packageRoot);
|
|
680
|
+
let currentReceipt = null;
|
|
681
|
+
try {
|
|
682
|
+
const receiptArtifact = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
|
|
683
|
+
currentReceipt = receiptArtifact?.value;
|
|
684
|
+
} catch {}
|
|
685
|
+
const recoveryAuth = validateCompletionRecoveryAuthorization({
|
|
686
|
+
state,
|
|
687
|
+
receipt: currentReceipt,
|
|
688
|
+
events: ledger.events,
|
|
689
|
+
});
|
|
690
|
+
recoveryAuthorized = recoveryAuth.authorized;
|
|
691
|
+
} catch {
|
|
692
|
+
recoveryAuthorized = false;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
645
695
|
return result({
|
|
646
696
|
...context,
|
|
647
|
-
nextAction:
|
|
648
|
-
|
|
649
|
-
|
|
697
|
+
nextAction: recoveryAuthorized
|
|
698
|
+
? NEXT_ACTIONS.ENTER_VERIFYING
|
|
699
|
+
: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
700
|
+
commands: recoveryAuthorized
|
|
701
|
+
? [commandFor(NEXT_ACTIONS.ENTER_VERIFYING)]
|
|
702
|
+
: [],
|
|
703
|
+
reasons: [...readiness.invalid, ...readiness.partial, ...readiness.missing]
|
|
650
704
|
.map((item) => artifactError(
|
|
651
|
-
|
|
652
|
-
|
|
705
|
+
readiness.missing.some((candidate) => candidate.id === item.id)
|
|
706
|
+
? "E_EVIDENCE_REQUIRED"
|
|
707
|
+
: item.reasonCode ?? "E_EVIDENCE_PARTIAL",
|
|
708
|
+
`Evidence is not ready: ${item.text}`,
|
|
653
709
|
[ARTIFACT_PATHS.state],
|
|
654
710
|
)),
|
|
655
711
|
requiredArtifacts,
|
|
@@ -707,6 +763,26 @@ export async function getNextAction({ target, packageRoot } = {}) {
|
|
|
707
763
|
}
|
|
708
764
|
const completion = await evaluateCompletion({ target, packageRoot });
|
|
709
765
|
if (completion.status !== "VALID") {
|
|
766
|
+
const terminalPendingErrors = completion.errors.filter((err) => (
|
|
767
|
+
err.code === "E_PUBLICATION_REQUIREMENT_PENDING" || err.code === "E_PRODUCTION_REQUIREMENT_PENDING"
|
|
768
|
+
));
|
|
769
|
+
if (terminalPendingErrors.length > 0 && terminalPendingErrors.length === completion.errors.length) {
|
|
770
|
+
const terminalPendingReqs = terminalPendingErrors.map((err) => {
|
|
771
|
+
const reqId = err.requirementId;
|
|
772
|
+
const matchingReq = evidence.requirements.find((r) => r.id === reqId)
|
|
773
|
+
?? evidence.requirements.find((r) => r.text === reqId)
|
|
774
|
+
?? classifyRequirement(reqId ?? err.message);
|
|
775
|
+
return matchingReq;
|
|
776
|
+
});
|
|
777
|
+
return result({
|
|
778
|
+
...context,
|
|
779
|
+
nextAction: NEXT_ACTIONS.RECORD_TERMINAL_RESULT,
|
|
780
|
+
commands: ["forgeloop record-terminal-result"],
|
|
781
|
+
commandSpecs: terminalPendingReqs.map(recordTerminalResultCommandSpec),
|
|
782
|
+
reasons: completion.errors,
|
|
783
|
+
requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt, ARTIFACT_PATHS.events],
|
|
784
|
+
});
|
|
785
|
+
}
|
|
710
786
|
return result({
|
|
711
787
|
...context,
|
|
712
788
|
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
package/src/core/phase.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
2
2
|
import { readContract } from "./contract.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
appendProtocolEvent,
|
|
5
|
+
LIFECYCLE_MILESTONES,
|
|
6
|
+
validateCompletionRecoveryAuthorization,
|
|
7
|
+
validateEventLedger,
|
|
8
|
+
validateStateLedgerCoherence,
|
|
9
|
+
} from "./events.js";
|
|
4
10
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
5
11
|
import { assertWorkPhase, isValidTransition } from "./protocol.js";
|
|
6
12
|
import { readWorkState, writeWorkState } from "./work-state.js";
|
|
@@ -25,6 +31,7 @@ const PHASE_EVENTS = Object.freeze({
|
|
|
25
31
|
PLANNED: "PLAN_RECORDED",
|
|
26
32
|
EXECUTING: "EXECUTION_STARTED",
|
|
27
33
|
VERIFYING: "VERIFICATION_STARTED",
|
|
34
|
+
REVIEWING: "REVIEW_STARTED",
|
|
28
35
|
COMPLETE: "COMPLETION_VALIDATED",
|
|
29
36
|
});
|
|
30
37
|
|
|
@@ -127,7 +134,9 @@ async function assertPhasePrerequisites(target, state, toPhase, packageRoot) {
|
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
export async function advanceWorkState(target, toPhase,
|
|
137
|
+
export async function advanceWorkState(target, toPhase, options = {}) {
|
|
138
|
+
const normalizedOptions = typeof options === "string" ? { packageRoot: options } : options;
|
|
139
|
+
const { packageRoot, now = new Date().toISOString() } = normalizedOptions;
|
|
131
140
|
assertWorkPhase(toPhase);
|
|
132
141
|
const state = await readWorkState(target, packageRoot);
|
|
133
142
|
if (!state) throw phaseError("E_PHASE_PREREQUISITE_MISSING", "Cannot advance without work state", [ARTIFACT_PATHS.state]);
|
|
@@ -148,11 +157,50 @@ export async function advanceWorkState(target, toPhase, { packageRoot, now = new
|
|
|
148
157
|
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.state],
|
|
149
158
|
);
|
|
150
159
|
}
|
|
160
|
+
const coherenceErrors = validateStateLedgerCoherence(state, ledger.events);
|
|
161
|
+
if (coherenceErrors.length > 0) {
|
|
162
|
+
throw phaseError(coherenceErrors[0].code, coherenceErrors[0].message, [ARTIFACT_PATHS.state, ARTIFACT_PATHS.events]);
|
|
163
|
+
}
|
|
151
164
|
const eventType = PHASE_EVENTS[toPhase];
|
|
152
|
-
|
|
165
|
+
const reenteringVerification = toPhase === "VERIFYING" && ["CORRECTING", "REVIEWING"].includes(state.phase);
|
|
166
|
+
if (toPhase === "VERIFYING" && state.phase === "REVIEWING") {
|
|
167
|
+
let currentReceipt = null;
|
|
168
|
+
try {
|
|
169
|
+
const receiptArtifact = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
|
|
170
|
+
currentReceipt = receiptArtifact?.value;
|
|
171
|
+
} catch {
|
|
172
|
+
// If receipt is not present, pass null
|
|
173
|
+
}
|
|
174
|
+
const recoveryAuth = validateCompletionRecoveryAuthorization({
|
|
175
|
+
state,
|
|
176
|
+
receipt: currentReceipt,
|
|
177
|
+
events: ledger.events,
|
|
178
|
+
});
|
|
179
|
+
if (!recoveryAuth.authorized) {
|
|
180
|
+
const firstError = recoveryAuth.errors[0] ?? {};
|
|
181
|
+
throw phaseError(
|
|
182
|
+
firstError.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
|
|
183
|
+
firstError.message ?? "REVIEWING -> VERIFYING requires authorized completion recovery",
|
|
184
|
+
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.events],
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (toPhase === "VERIFYING" && state.phase === "CORRECTING") {
|
|
189
|
+
if (typeof state.diagnosedHypothesis !== "string" || !state.diagnosedHypothesis.trim()) {
|
|
190
|
+
throw phaseError(
|
|
191
|
+
"E_PHASE_PREREQUISITE_MISSING",
|
|
192
|
+
"CORRECTING -> VERIFYING requires a diagnosed hypothesis",
|
|
193
|
+
[ARTIFACT_PATHS.state],
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const repeatedReview = state.phase === "VERIFYING" && toPhase === "REVIEWING"
|
|
198
|
+
&& ledger.events.some((event) => event.taskId === state.taskId && event.event === "REVIEW_STARTED");
|
|
199
|
+
if (eventType && !reenteringVerification && !repeatedReview
|
|
200
|
+
&& ledger.events.some((event) => event.taskId === state.taskId && event.event === eventType)) {
|
|
153
201
|
throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `Lifecycle milestone already exists: ${eventType}`, [ARTIFACT_PATHS.events]);
|
|
154
202
|
}
|
|
155
|
-
const milestoneIndex = LIFECYCLE_MILESTONES.indexOf(eventType);
|
|
203
|
+
const milestoneIndex = reenteringVerification || repeatedReview ? -1 : LIFECYCLE_MILESTONES.indexOf(eventType);
|
|
156
204
|
if (milestoneIndex >= 0) {
|
|
157
205
|
const lastMilestone = ledger.events.reduce((last, event) => Math.max(last, LIFECYCLE_MILESTONES.indexOf(event.event)), -1);
|
|
158
206
|
if (lastMilestone !== milestoneIndex - 1) {
|
|
@@ -166,6 +214,10 @@ export async function advanceWorkState(target, toPhase, { packageRoot, now = new
|
|
|
166
214
|
phase: toPhase,
|
|
167
215
|
lastUpdated: now,
|
|
168
216
|
};
|
|
217
|
+
if (toPhase === "VERIFYING") {
|
|
218
|
+
next.verificationCycle = reenteringVerification ? (state.verificationCycle ?? 1) + 1 : (state.verificationCycle ?? 1);
|
|
219
|
+
}
|
|
220
|
+
if (reenteringVerification) delete next.lastCompletionAttempt;
|
|
169
221
|
let nextReceipt = null;
|
|
170
222
|
try {
|
|
171
223
|
const receipt = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
|
|
@@ -187,10 +239,12 @@ export async function advanceWorkState(target, toPhase, { packageRoot, now = new
|
|
|
187
239
|
receipt: receipt.value,
|
|
188
240
|
requiredEvidence,
|
|
189
241
|
requireRequiredChecks: false,
|
|
242
|
+
requireReceiptStateFingerprint: false,
|
|
190
243
|
});
|
|
191
244
|
nextReceipt = await createReceipt({
|
|
192
245
|
...receipt.value,
|
|
193
246
|
stateFingerprint: canonicalFingerprint(next),
|
|
247
|
+
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
194
248
|
}, packageRoot);
|
|
195
249
|
assertCompletionRelationships({
|
|
196
250
|
contract,
|
|
@@ -207,6 +261,15 @@ export async function advanceWorkState(target, toPhase, { packageRoot, now = new
|
|
|
207
261
|
if (nextReceipt) {
|
|
208
262
|
await writeJsonArtifact(target, ARTIFACT_PATHS.receipt, nextReceipt, "execution-receipt", packageRoot);
|
|
209
263
|
}
|
|
210
|
-
if (eventType) await appendProtocolEvent(target, {
|
|
264
|
+
if (eventType) await appendProtocolEvent(target, {
|
|
265
|
+
taskId: state.taskId,
|
|
266
|
+
event: eventType,
|
|
267
|
+
at: now,
|
|
268
|
+
details: eventType === "VERIFICATION_STARTED"
|
|
269
|
+
? { verificationCycle: next.verificationCycle }
|
|
270
|
+
: eventType === "REVIEW_STARTED"
|
|
271
|
+
? { verificationCycle: next.verificationCycle ?? 1 }
|
|
272
|
+
: undefined,
|
|
273
|
+
}, packageRoot);
|
|
211
274
|
return next;
|
|
212
275
|
}
|
package/src/core/protocol.js
CHANGED
|
@@ -36,16 +36,35 @@ export const FAILURE_CODES = Object.freeze([
|
|
|
36
36
|
"E_EVIDENCE_REQUIRED",
|
|
37
37
|
"E_EVIDENCE_KIND_INVALID",
|
|
38
38
|
"E_EVIDENCE_STALE",
|
|
39
|
+
"E_EVIDENCE_PARTIAL",
|
|
40
|
+
"E_EVIDENCE_INVALID",
|
|
41
|
+
"E_FUTURE_LIFECYCLE_EVIDENCE",
|
|
42
|
+
"E_CIRCULAR_COMPLETION_REQUIREMENT",
|
|
43
|
+
"E_MIXED_TERMINAL_REQUIREMENT",
|
|
39
44
|
"E_EVIDENCE_COVERAGE_PARTIAL",
|
|
45
|
+
"E_EVIDENCE_COVERAGE_INVALID",
|
|
46
|
+
"E_CHECK_INVALID",
|
|
40
47
|
"E_CHECK_STATUS_CONTRADICTION",
|
|
41
48
|
"E_PROFILE_SOURCE_MISSING",
|
|
42
49
|
"E_PROFILE_SOURCE_UNKNOWN",
|
|
43
50
|
"E_PROFILE_SOURCE_MISCLASSIFIED",
|
|
51
|
+
"E_PROFILE_UNVERIFIED",
|
|
52
|
+
"E_RECEIPT_MISSING",
|
|
53
|
+
"E_RECEIPT_INVALID",
|
|
54
|
+
"E_RECEIPT_TASK_MISMATCH",
|
|
44
55
|
"E_RECEIPT_ROUTE_MISMATCH",
|
|
45
56
|
"E_RECEIPT_STATE_MISMATCH",
|
|
46
57
|
"E_RECEIPT_CONTRACT_MISMATCH",
|
|
47
58
|
"E_RECEIPT_PATH_MISMATCH",
|
|
59
|
+
"E_RECEIPT_CYCLE_MISMATCH",
|
|
48
60
|
"E_COMPLETION_REJECTED",
|
|
61
|
+
"E_COMPLETION_RECOVERY_UNAUTHORIZED",
|
|
62
|
+
"E_COMPLETION_REJECTION_LEDGER_MISMATCH",
|
|
63
|
+
"E_COMPLETION_REJECTION_STATE_FINGERPRINT_MISMATCH",
|
|
64
|
+
"E_COMPLETION_REJECTION_RECEIPT_FINGERPRINT_MISMATCH",
|
|
65
|
+
"E_STATE_MISSING",
|
|
66
|
+
"E_STATE_TASK_MISMATCH",
|
|
67
|
+
"E_STATE_LEDGER_DIVERGENCE",
|
|
49
68
|
"E_PUBLICATION_CLAIM_UNVERIFIED",
|
|
50
69
|
"E_PRODUCTION_READINESS_UNVERIFIED",
|
|
51
70
|
"E_STATE_MISSING_AFTER_PREFLIGHT_READY",
|
|
@@ -54,6 +73,14 @@ export const FAILURE_CODES = Object.freeze([
|
|
|
54
73
|
"E_PREFLIGHT_EVENT_MISSING",
|
|
55
74
|
"E_PREFLIGHT_GATE_EVENT_MISSING",
|
|
56
75
|
"E_PREFLIGHT_GATES_STALE",
|
|
76
|
+
"E_TERMINAL_REQUIREMENT_PENDING",
|
|
77
|
+
"E_PUBLICATION_REQUIREMENT_PENDING",
|
|
78
|
+
"E_PRODUCTION_REQUIREMENT_PENDING",
|
|
79
|
+
"E_FUTURE_TERMINAL_EVIDENCE",
|
|
80
|
+
"E_TERMINAL_REQUIREMENT_UNKNOWN",
|
|
81
|
+
"E_TERMINAL_REQUIREMENT_NOT_TERMINAL",
|
|
82
|
+
"E_TERMINAL_REQUIREMENT_TYPE_MISMATCH",
|
|
83
|
+
"E_TERMINAL_STATUS_REGRESSION",
|
|
57
84
|
]);
|
|
58
85
|
|
|
59
86
|
export const WORK_PHASES = Object.freeze([
|
|
@@ -97,7 +124,7 @@ export const WORK_TRANSITIONS = Object.freeze({
|
|
|
97
124
|
VERIFYING: ["DIAGNOSING", "REVIEWING"],
|
|
98
125
|
DIAGNOSING: ["CORRECTING"],
|
|
99
126
|
CORRECTING: ["VERIFYING"],
|
|
100
|
-
REVIEWING: ["COMPLETE", "CORRECTING"],
|
|
127
|
+
REVIEWING: ["COMPLETE", "CORRECTING", "VERIFYING"],
|
|
101
128
|
COMPLETE: [],
|
|
102
129
|
BLOCKED: [],
|
|
103
130
|
});
|
package/src/core/receipt.js
CHANGED
|
@@ -58,6 +58,10 @@ function assertKnownGuides(guides) {
|
|
|
58
58
|
|
|
59
59
|
export function assertReceiptSemantics(receipt) {
|
|
60
60
|
const evidence = receipt.evidence ?? [];
|
|
61
|
+
if (receipt.verificationCycle !== undefined
|
|
62
|
+
&& (!Number.isInteger(receipt.verificationCycle) || receipt.verificationCycle < 1)) {
|
|
63
|
+
throw new Error("receipt.verificationCycle must be a positive integer");
|
|
64
|
+
}
|
|
61
65
|
assertEvidenceList(evidence, "receipt.evidence");
|
|
62
66
|
|
|
63
67
|
for (const [index, check] of receipt.checks.entries()) {
|
|
@@ -145,6 +149,7 @@ export async function createReceipt(input, packageRoot) {
|
|
|
145
149
|
contractFingerprint: input.contractFingerprint,
|
|
146
150
|
...(input.routeFingerprint !== undefined ? { routeFingerprint: input.routeFingerprint } : {}),
|
|
147
151
|
...(input.stateFingerprint !== undefined ? { stateFingerprint: input.stateFingerprint } : {}),
|
|
152
|
+
...(input.verificationCycle !== undefined ? { verificationCycle: input.verificationCycle } : {}),
|
|
148
153
|
status: input.status ?? "in-progress",
|
|
149
154
|
...(input.taskStatus !== undefined ? { taskStatus: input.taskStatus } : {}),
|
|
150
155
|
...(input.verificationStatus !== undefined ? { verificationStatus: input.verificationStatus } : {}),
|
|
@@ -48,7 +48,29 @@ function display(value) {
|
|
|
48
48
|
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function
|
|
51
|
+
function resolveRef(ref, rootSchema) {
|
|
52
|
+
if (typeof ref !== "string" || !rootSchema) return null;
|
|
53
|
+
if (ref.startsWith("#/")) {
|
|
54
|
+
const parts = ref.slice(2).split("/");
|
|
55
|
+
let target = rootSchema;
|
|
56
|
+
for (const part of parts) {
|
|
57
|
+
target = target?.[part];
|
|
58
|
+
}
|
|
59
|
+
return target ?? null;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validate(value, schema, location, errors, rootSchema = schema) {
|
|
65
|
+
if (!schema || typeof schema !== "object") return;
|
|
66
|
+
if (schema.$ref) {
|
|
67
|
+
const target = resolveRef(schema.$ref, rootSchema);
|
|
68
|
+
if (target) {
|
|
69
|
+
validate(value, target, location, errors, rootSchema);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
if (schema.const !== undefined && value !== schema.const) {
|
|
53
75
|
errors.push(`${location}: expected ${display(schema.const)}`);
|
|
54
76
|
return;
|
|
@@ -62,7 +84,7 @@ function validate(value, schema, location, errors) {
|
|
|
62
84
|
if (schema.oneOf) {
|
|
63
85
|
const matches = schema.oneOf.filter((candidate) => {
|
|
64
86
|
const candidateErrors = [];
|
|
65
|
-
validate(value, candidate, location, candidateErrors);
|
|
87
|
+
validate(value, candidate, location, candidateErrors, rootSchema);
|
|
66
88
|
return candidateErrors.length === 0;
|
|
67
89
|
});
|
|
68
90
|
if (matches.length !== 1) errors.push(`${location}: expected exactly one matching schema`);
|
|
@@ -91,7 +113,7 @@ function validate(value, schema, location, errors) {
|
|
|
91
113
|
errors.push(`${location}: must contain at most ${schema.maxItems} items`);
|
|
92
114
|
}
|
|
93
115
|
if (schema.items) {
|
|
94
|
-
value.forEach((item, index) => validate(item, schema.items, `${location}[${index}]`, errors));
|
|
116
|
+
value.forEach((item, index) => validate(item, schema.items, `${location}[${index}]`, errors, rootSchema));
|
|
95
117
|
}
|
|
96
118
|
}
|
|
97
119
|
|
|
@@ -105,7 +127,7 @@ function validate(value, schema, location, errors) {
|
|
|
105
127
|
const properties = schema.properties ?? {};
|
|
106
128
|
for (const [key, child] of Object.entries(properties)) {
|
|
107
129
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
108
|
-
validate(value[key], child, `${location}.${key}`, errors);
|
|
130
|
+
validate(value[key], child, `${location}.${key}`, errors, rootSchema);
|
|
109
131
|
}
|
|
110
132
|
}
|
|
111
133
|
|
|
@@ -118,7 +140,7 @@ function validate(value, schema, location, errors) {
|
|
|
118
140
|
} else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
119
141
|
for (const key of Object.keys(value)) {
|
|
120
142
|
if (!Object.prototype.hasOwnProperty.call(properties, key)) {
|
|
121
|
-
validate(value[key], schema.additionalProperties, `${location}.${key}`, errors);
|
|
143
|
+
validate(value[key], schema.additionalProperties, `${location}.${key}`, errors, rootSchema);
|
|
122
144
|
}
|
|
123
145
|
}
|
|
124
146
|
}
|
|
@@ -128,7 +150,7 @@ function validate(value, schema, location, errors) {
|
|
|
128
150
|
export function validateSchema(value, schema, { label = "$" } = {}) {
|
|
129
151
|
assertJsonLimits(value, label);
|
|
130
152
|
const errors = [];
|
|
131
|
-
validate(value, schema, label, errors);
|
|
153
|
+
validate(value, schema, label, errors, schema);
|
|
132
154
|
return errors;
|
|
133
155
|
}
|
|
134
156
|
|
package/src/core/templates.js
CHANGED
package/src/core/work-state.js
CHANGED
|
@@ -159,6 +159,31 @@ export function assertWorkStateSemantics(state) {
|
|
|
159
159
|
throw new WorkStateError(`Invalid work-state transition: ${state.previousPhase} -> ${state.phase}`);
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
+
if (state.verificationCycle !== undefined && (!Number.isInteger(state.verificationCycle) || state.verificationCycle < 1)) {
|
|
163
|
+
throw new WorkStateError("verificationCycle must be a positive integer");
|
|
164
|
+
}
|
|
165
|
+
if (state.lastCompletionAttempt !== undefined) {
|
|
166
|
+
if (!state.lastCompletionAttempt || typeof state.lastCompletionAttempt !== "object" || Array.isArray(state.lastCompletionAttempt)) {
|
|
167
|
+
throw new WorkStateError("lastCompletionAttempt must be an object");
|
|
168
|
+
}
|
|
169
|
+
if (state.lastCompletionAttempt.status !== "REJECTED") {
|
|
170
|
+
throw new WorkStateError("lastCompletionAttempt.status must be REJECTED");
|
|
171
|
+
}
|
|
172
|
+
if (!Array.isArray(state.lastCompletionAttempt.reasonCodes)
|
|
173
|
+
|| state.lastCompletionAttempt.reasonCodes.some((code) => typeof code !== "string" || !code.trim())) {
|
|
174
|
+
throw new WorkStateError("lastCompletionAttempt.reasonCodes must be an array of non-empty strings");
|
|
175
|
+
}
|
|
176
|
+
if (!Array.isArray(state.lastCompletionAttempt.missingRequirementIds)
|
|
177
|
+
|| state.lastCompletionAttempt.missingRequirementIds.some((id) => typeof id !== "string" || !id.trim())) {
|
|
178
|
+
throw new WorkStateError("lastCompletionAttempt.missingRequirementIds must be an array of non-empty strings");
|
|
179
|
+
}
|
|
180
|
+
if (!Number.isInteger(state.lastCompletionAttempt.verificationCycle) || state.lastCompletionAttempt.verificationCycle < 1) {
|
|
181
|
+
throw new WorkStateError("lastCompletionAttempt.verificationCycle must be a positive integer");
|
|
182
|
+
}
|
|
183
|
+
if (typeof state.lastCompletionAttempt.timestamp !== "string" || !state.lastCompletionAttempt.timestamp.trim()) {
|
|
184
|
+
throw new WorkStateError("lastCompletionAttempt.timestamp must be a non-empty string");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
162
187
|
for (const failure of state.failures) {
|
|
163
188
|
if (failure?.failureClass !== undefined) assertFailureClass(failure.failureClass);
|
|
164
189
|
}
|
|
@@ -191,6 +216,8 @@ export function createWorkState(input) {
|
|
|
191
216
|
verificationEvidence: [...(input.verificationEvidence ?? [])],
|
|
192
217
|
lastUpdated: input.lastUpdated ?? new Date().toISOString(),
|
|
193
218
|
};
|
|
219
|
+
if (input.verificationCycle !== undefined) state.verificationCycle = input.verificationCycle;
|
|
220
|
+
if (input.lastCompletionAttempt !== undefined) state.lastCompletionAttempt = structuredClone(input.lastCompletionAttempt);
|
|
194
221
|
if (input.previousPhase !== undefined) state.previousPhase = input.previousPhase;
|
|
195
222
|
if (input.diagnosedHypothesis !== undefined) state.diagnosedHypothesis = input.diagnosedHypothesis;
|
|
196
223
|
return assertWorkStateSemantics(state);
|