@cassiomc1/forgeloop 1.3.0 → 1.6.0
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/.github/copilot-instructions.md +1 -0
- package/AGENTS.md +1 -0
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +20 -8
- package/EXECUTION_STATE.md +60 -0
- package/LOOP_ENGINEERING.md +135 -5
- package/LOOP_SYSTEM_DESIGN.md +54 -1
- package/PROTOCOL_INTEGRATION.md +87 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +69 -9
- package/TERMINOLOGY.md +15 -0
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/THREAT_MODEL.md +59 -1
- package/docs/ARTIFACT_REFERENCE.md +183 -0
- package/docs/CLI_REFERENCE.md +391 -6
- package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
- package/docs/DIAGNOSTIC_MODEL.md +181 -0
- package/docs/DOCUMENTATION_GUIDE.md +36 -13
- package/docs/EXECUTION_TRACE.md +76 -0
- package/docs/GETTING_STARTED.md +1 -0
- package/docs/MCP.md +159 -0
- package/docs/RECIPES.md +149 -0
- package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
- package/docs/TROUBLESHOOTING.md +217 -3
- package/docs/UNIVERSAL_INTEGRATION.md +48 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
- package/docs/diagrams/README.md +55 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
- package/package.json +21 -8
- package/schemas/action.schema.json +100 -0
- package/schemas/approval.schema.json +51 -0
- package/schemas/capability-policy.schema.json +41 -0
- package/schemas/diagnostic-case.schema.json +85 -0
- package/schemas/execution-receipt.schema.json +16 -0
- package/schemas/hypothesis-disposition.schema.json +16 -0
- package/schemas/intervention.schema.json +27 -0
- package/schemas/policy-lock.schema.json +1 -0
- package/schemas/policy-snapshot.schema.json +2 -0
- package/schemas/task-recovery.schema.json +61 -0
- package/schemas/trajectory-evaluation.schema.json +64 -0
- package/schemas/trajectory-scenario.schema.json +42 -0
- package/src/cli.js +267 -347
- package/src/commands/action-authorize.js +41 -0
- package/src/commands/action-propose.js +10 -0
- package/src/commands/action-reconcile.js +10 -0
- package/src/commands/action-record.js +47 -0
- package/src/commands/action-show.js +10 -0
- package/src/commands/action-verify.js +10 -0
- package/src/commands/advance.js +7 -2
- package/src/commands/approval-request.js +64 -0
- package/src/commands/approval-resolve.js +10 -0
- package/src/commands/audit.js +5 -0
- package/src/commands/baseline.js +3 -3
- package/src/commands/eval.js +6 -0
- package/src/commands/history.js +18 -0
- package/src/commands/init.js +2 -2
- package/src/commands/inspect.js +55 -0
- package/src/commands/metrics.js +7 -0
- package/src/commands/next.js +8 -2
- package/src/commands/policy-discover.js +2 -2
- package/src/commands/progress.js +6 -2
- package/src/commands/record-diagnosis.js +37 -1
- package/src/commands/record-hypothesis-disposition.js +45 -0
- package/src/commands/record-intervention.js +35 -0
- package/src/commands/reflect.js +38 -0
- package/src/commands/report.js +9 -1
- package/src/commands/run-action.js +18 -0
- package/src/commands/status.js +17 -0
- package/src/commands/task-create.js +39 -1
- package/src/commands/task-list.js +14 -1
- package/src/commands/task-lock-status.js +2 -2
- package/src/commands/task-recover.js +202 -0
- package/src/commands/task-repair-legacy-recovery.js +417 -0
- package/src/commands/task-resume.js +172 -0
- package/src/commands/task-scope.js +23 -4
- package/src/commands/task-show.js +18 -4
- package/src/commands/trace.js +34 -0
- package/src/commands/validate-protocol.js +40 -15
- package/src/core/action-authorization.js +106 -0
- package/src/core/action-constants.js +86 -0
- package/src/core/action-execution.js +105 -0
- package/src/core/action-ledger-projection.js +302 -0
- package/src/core/action-model.js +581 -0
- package/src/core/action-readiness.js +141 -0
- package/src/core/action-reconciliation-policy.js +49 -0
- package/src/core/action-reconciliation.js +66 -0
- package/src/core/action-verification.js +111 -0
- package/src/core/actions.js +462 -0
- package/src/core/approvals.js +405 -0
- package/src/core/artifact-registry.js +60 -0
- package/src/core/audit.js +45 -4
- package/src/core/bundles.js +30 -0
- package/src/core/capability-policy.js +226 -0
- package/src/core/cli-command-definitions.js +260 -5
- package/src/core/command-executors.js +543 -0
- package/src/core/command-input.js +107 -0
- package/src/core/command-runtime.js +117 -0
- package/src/core/completion-artifacts.js +39 -15
- package/src/core/completion-ownership.js +88 -0
- package/src/core/completion-recovery-rebind.js +194 -0
- package/src/core/completion.js +70 -0
- package/src/core/continuity-reconciliation.js +24 -5
- package/src/core/diagnostic-model.js +396 -0
- package/src/core/diagnostic-projection.js +51 -0
- package/src/core/diagnostic-record.js +360 -0
- package/src/core/error-codes.js +461 -1
- package/src/core/events.js +171 -2
- package/src/core/execution-prerequisites.js +4 -1
- package/src/core/execution.js +26 -188
- package/src/core/failure-signature.js +70 -0
- package/src/core/failure-surface.js +57 -0
- package/src/core/filesystem.js +55 -6
- package/src/core/history.js +110 -0
- package/src/core/hypothesis-projection.js +85 -0
- package/src/core/information-gain-projection.js +283 -0
- package/src/core/information-gain.js +138 -0
- package/src/core/inspect.js +132 -7
- package/src/core/integration-invocation-policy.js +217 -0
- package/src/core/integration-limits.js +20 -0
- package/src/core/integration-resources.js +178 -0
- package/src/core/next-action-model.js +94 -0
- package/src/core/next-action.js +490 -3
- package/src/core/phase.js +42 -22
- package/src/core/policy-engine.js +113 -6
- package/src/core/preflight-consistency.js +31 -5
- package/src/core/preflight.js +19 -2
- package/src/core/prepared-execution.js +227 -0
- package/src/core/progress.js +41 -4
- package/src/core/project-root.js +21 -0
- package/src/core/protocol-info.js +61 -0
- package/src/core/protocol.js +14 -0
- package/src/core/receipt.js +1 -0
- package/src/core/reconcile-closure.js +35 -10
- package/src/core/recovery-history.js +116 -0
- package/src/core/reflection.js +305 -0
- package/src/core/resumability.js +57 -3
- package/src/core/schema-validation.js +9 -0
- package/src/core/strategy-analysis.js +97 -0
- package/src/core/task-claim-state.js +272 -0
- package/src/core/task-command.js +5 -1
- package/src/core/task-conflict-inspection.js +321 -0
- package/src/core/task-context.js +32 -29
- package/src/core/task-discovery.js +14 -1
- package/src/core/task-lock.js +216 -22
- package/src/core/task-paths.js +31 -2
- package/src/core/task-recovery-migration.js +192 -0
- package/src/core/task-recovery.js +205 -0
- package/src/core/task-scope.js +33 -1
- package/src/core/task-snapshot.js +53 -0
- package/src/core/templates.js +9 -0
- package/src/core/trace.js +548 -0
- package/src/core/trajectory-evaluation.js +71 -0
- package/src/core/trajectory-metrics.js +80 -0
- package/src/core/transaction.js +36 -2
- package/src/core/work-state.js +10 -5
- package/src/integration.js +47 -0
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { readAction, listActions } from "./actions.js";
|
|
2
|
+
import { projectActionLedger } from "./action-ledger-projection.js";
|
|
3
|
+
|
|
4
|
+
export const ACTION_READINESS_STATUSES = Object.freeze([
|
|
5
|
+
"SATISFIED",
|
|
6
|
+
"PENDING",
|
|
7
|
+
"FAILED",
|
|
8
|
+
"AMBIGUOUS",
|
|
9
|
+
"UNTRUSTED",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function result(actionId, status, reasons, extra = {}) {
|
|
13
|
+
return {
|
|
14
|
+
actionId,
|
|
15
|
+
status,
|
|
16
|
+
reasons,
|
|
17
|
+
...extra,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Canonical readiness projection for one durable action. Completion truth is
|
|
23
|
+
* derived here and nowhere else (INV-VERIFY-02): raw `state === "VERIFIED"`
|
|
24
|
+
* labels are never sufficient without trusted authorization and canonical
|
|
25
|
+
* verification evidence replayed from the ledger.
|
|
26
|
+
*/
|
|
27
|
+
export async function evaluateActionReadiness({
|
|
28
|
+
target,
|
|
29
|
+
packageRoot,
|
|
30
|
+
taskId,
|
|
31
|
+
action,
|
|
32
|
+
}) {
|
|
33
|
+
const resolved = action ?? await readAction(target, { packageRoot, taskId, actionId: action?.actionId });
|
|
34
|
+
const ledger = await projectActionLedger({
|
|
35
|
+
target,
|
|
36
|
+
packageRoot,
|
|
37
|
+
taskId,
|
|
38
|
+
actionId: resolved.actionId,
|
|
39
|
+
artifact: resolved,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!ledger.valid) {
|
|
43
|
+
return result(resolved.actionId, "UNTRUSTED", [
|
|
44
|
+
"action ledger chronology is invalid or diverges from the artifact",
|
|
45
|
+
...ledger.errors.map((error) => error.message),
|
|
46
|
+
], { authorization: ledger.authorization, verification: ledger.verification });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// REQUIRE_APPROVAL authorizations bind the exact resolved approval content;
|
|
50
|
+
// any post-authorization mutation of the approval artifact must be
|
|
51
|
+
// readiness/audit-visible (INV-FINAL-APPROVAL-01).
|
|
52
|
+
const authorizationDetails = ledger.authorization.details;
|
|
53
|
+
if (authorizationDetails?.capabilityDecision === "REQUIRE_APPROVAL") {
|
|
54
|
+
try {
|
|
55
|
+
const { validateBoundApprovalFingerprint } = await import("./approvals.js");
|
|
56
|
+
await validateBoundApprovalFingerprint(target, {
|
|
57
|
+
packageRoot,
|
|
58
|
+
taskId,
|
|
59
|
+
approvalId: authorizationDetails.approvalId,
|
|
60
|
+
expectedFingerprint: authorizationDetails.approvalFingerprint,
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return result(resolved.actionId, "UNTRUSTED", [
|
|
64
|
+
`bound approval integrity failed: ${error.message}`,
|
|
65
|
+
], { authorization: ledger.authorization, verification: ledger.verification });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (resolved.state === "COMMIT_UNKNOWN") {
|
|
70
|
+
return result(resolved.actionId, "AMBIGUOUS", [
|
|
71
|
+
"external commit state is unknown; reconcile through a trusted boundary",
|
|
72
|
+
], { authorization: ledger.authorization, verification: ledger.verification });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (resolved.state === "FAILED" || resolved.state === "CANCELLED") {
|
|
76
|
+
return result(resolved.actionId, "FAILED", [
|
|
77
|
+
`action is ${resolved.state}`,
|
|
78
|
+
], { authorization: ledger.authorization, verification: ledger.verification });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (resolved.state !== "VERIFIED") {
|
|
82
|
+
return result(resolved.actionId, "PENDING", [
|
|
83
|
+
`action is ${resolved.state}; verification requires canonical independent evidence`,
|
|
84
|
+
], { authorization: ledger.authorization, verification: ledger.verification });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A required action without a requirement can never be strongly verified:
|
|
88
|
+
// legacy artifacts stay readable but cannot become trusted-satisfied
|
|
89
|
+
// (INV-FINAL-VERIFY-02).
|
|
90
|
+
if (
|
|
91
|
+
resolved.requiredForCompletion
|
|
92
|
+
&& (typeof resolved.requirement !== "string" || resolved.requirement.length === 0)
|
|
93
|
+
) {
|
|
94
|
+
return result(resolved.actionId, "UNTRUSTED", [
|
|
95
|
+
"required action has no canonical requirement binding",
|
|
96
|
+
], { authorization: ledger.authorization, verification: ledger.verification });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// VERIFIED label present: trust requires modern authorization evidence.
|
|
100
|
+
if (!ledger.authorization.valid) {
|
|
101
|
+
return result(resolved.actionId, "UNTRUSTED", [
|
|
102
|
+
"authorization evidence is invalid or legacy pre-hardening",
|
|
103
|
+
], { authorization: ledger.authorization, verification: ledger.verification });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!ledger.verification.valid) {
|
|
107
|
+
return result(resolved.actionId, "UNTRUSTED", [
|
|
108
|
+
"verification evidence is missing or not canonical",
|
|
109
|
+
], { authorization: ledger.authorization, verification: ledger.verification });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result(resolved.actionId, "SATISFIED", [], {
|
|
113
|
+
authorization: ledger.authorization,
|
|
114
|
+
verification: ledger.verification,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Readiness summary over every required-for-completion action in a task.
|
|
120
|
+
*/
|
|
121
|
+
export async function evaluateRequiredActionReadiness({
|
|
122
|
+
target,
|
|
123
|
+
packageRoot,
|
|
124
|
+
taskId,
|
|
125
|
+
}) {
|
|
126
|
+
const actions = await listActions(target, { packageRoot, taskId });
|
|
127
|
+
const required = actions.filter((action) => action.requiredForCompletion);
|
|
128
|
+
const evaluated = [];
|
|
129
|
+
for (const action of required) {
|
|
130
|
+
evaluated.push(await evaluateActionReadiness({ target, packageRoot, taskId, action }));
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
total: required.length,
|
|
134
|
+
satisfied: evaluated.filter((item) => item.status === "SATISFIED").length,
|
|
135
|
+
unresolved: evaluated.filter((item) => item.status !== "SATISFIED").length,
|
|
136
|
+
ambiguous: evaluated.filter((item) => item.status === "AMBIGUOUS").length,
|
|
137
|
+
failed: evaluated.filter((item) => item.status === "FAILED").length,
|
|
138
|
+
untrusted: evaluated.filter((item) => item.status === "UNTRUSTED").length,
|
|
139
|
+
actions: evaluated,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
E_ACTION_RECONCILIATION_AUTHORITY_REQUIRED,
|
|
3
|
+
E_ACTION_RECONCILIATION_EVIDENCE_INVALID,
|
|
4
|
+
} from "./error-codes.js";
|
|
5
|
+
import { isTrustedHostAuthorityContext } from "./capability-policy.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Only these outcomes settle external commit state. They relax or resolve the
|
|
9
|
+
* COMMIT_UNKNOWN safety posture and therefore require trusted evidence;
|
|
10
|
+
* UNKNOWN is a pure observation and is always caller-recordable
|
|
11
|
+
* (INV-RECON-01, INV-RECON-02).
|
|
12
|
+
*/
|
|
13
|
+
export function reconciliationRequiresAuthority(outcome) {
|
|
14
|
+
return outcome === "COMMITTED" || outcome === "NOT_COMMITTED";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate that a reconciliation invocation may settle external state.
|
|
19
|
+
* Settlement requires:
|
|
20
|
+
* - a trusted host-boundary authority context (out-of-band), and
|
|
21
|
+
* - at least one bounded evidence reference binding the observation.
|
|
22
|
+
*/
|
|
23
|
+
export function assertReconciliationSettlementAllowed({ outcome, authorityContext, evidenceRefs }) {
|
|
24
|
+
if (!reconciliationRequiresAuthority(outcome)) return null;
|
|
25
|
+
|
|
26
|
+
if (!isTrustedHostAuthorityContext(authorityContext)) {
|
|
27
|
+
const error = new Error("settling external commit state requires trusted host attestation");
|
|
28
|
+
error.code = E_ACTION_RECONCILIATION_AUTHORITY_REQUIRED;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
if (
|
|
32
|
+
!Array.isArray(evidenceRefs)
|
|
33
|
+
|| evidenceRefs.length === 0
|
|
34
|
+
|| evidenceRefs.some((ref) => typeof ref !== "string" || !ref || ref.length > 256)
|
|
35
|
+
) {
|
|
36
|
+
const error = new Error("settling external commit state requires bounded non-empty evidence references");
|
|
37
|
+
error.code = E_ACTION_RECONCILIATION_EVIDENCE_INVALID;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
if (typeof authorityContext.grantRef !== "string" || !authorityContext.grantRef) {
|
|
41
|
+
const error = new Error("trusted settlement authority must carry a bounded grant reference");
|
|
42
|
+
error.code = E_ACTION_RECONCILIATION_AUTHORITY_REQUIRED;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
authorityKind: "HOST_ATTESTED",
|
|
47
|
+
authorityRef: authorityContext.grantRef,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readAction, transitionAction } from "./actions.js";
|
|
2
|
+
import { assertReconciliationSettlementAllowed } from "./action-reconciliation-policy.js";
|
|
3
|
+
import { E_ACTION_EVIDENCE_INVALID, E_ACTION_STATE_MISMATCH } from "./error-codes.js";
|
|
4
|
+
|
|
5
|
+
function reconciliationError(code, message) {
|
|
6
|
+
const error = new Error(message);
|
|
7
|
+
error.code = code;
|
|
8
|
+
return error;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reconcile a COMMIT_UNKNOWN action from observed external state.
|
|
13
|
+
*
|
|
14
|
+
* - UNKNOWN is a safe observation: the action remains COMMIT_UNKNOWN
|
|
15
|
+
* (INV-RECON-01).
|
|
16
|
+
* - COMMITTED settles the ambiguity as externally committed.
|
|
17
|
+
* - NOT_COMMITTED proves the external effect did not happen and returns the
|
|
18
|
+
* action to PROPOSED so any retry re-evaluates policy, approval, authority,
|
|
19
|
+
* and the task policy snapshot (INV-RECON-03). Stale authorization can
|
|
20
|
+
* never be reused.
|
|
21
|
+
*
|
|
22
|
+
* Settling outcomes (COMMITTED / NOT_COMMITTED) require trusted host
|
|
23
|
+
* attestation plus bounded evidence, supplied out-of-band (INV-RECON-02).
|
|
24
|
+
*/
|
|
25
|
+
export async function reconcileAction({ target, packageRoot, taskId, actionId, outcome,
|
|
26
|
+
evidenceRefs = [], observedAt = new Date().toISOString(), provenance = "EXTERNAL_OBSERVED",
|
|
27
|
+
authorityContext }) {
|
|
28
|
+
if (!["COMMITTED", "NOT_COMMITTED", "UNKNOWN"].includes(outcome)) {
|
|
29
|
+
throw reconciliationError(E_ACTION_EVIDENCE_INVALID,
|
|
30
|
+
"reconciliation outcome must be COMMITTED, NOT_COMMITTED, or UNKNOWN");
|
|
31
|
+
}
|
|
32
|
+
if (provenance !== "EXTERNAL_OBSERVED") {
|
|
33
|
+
throw reconciliationError(E_ACTION_EVIDENCE_INVALID,
|
|
34
|
+
"reconciliation provenance must be EXTERNAL_OBSERVED");
|
|
35
|
+
}
|
|
36
|
+
const settlement = assertReconciliationSettlementAllowed({ outcome, authorityContext, evidenceRefs });
|
|
37
|
+
|
|
38
|
+
const action = await readAction(target, { packageRoot, taskId, actionId });
|
|
39
|
+
if (action.state !== "COMMIT_UNKNOWN") {
|
|
40
|
+
throw reconciliationError(E_ACTION_STATE_MISMATCH,
|
|
41
|
+
`action ${actionId} is ${action.state}, not COMMIT_UNKNOWN`);
|
|
42
|
+
}
|
|
43
|
+
if (outcome === "UNKNOWN") {
|
|
44
|
+
const unchanged = await transitionAction(target, { packageRoot, taskId, actionId,
|
|
45
|
+
to: "COMMIT_UNKNOWN", expectedRevision: action.revision,
|
|
46
|
+
details: { reconciliationOutcome: outcome, evidenceRefs, observedAt, reconciliationAt: observedAt } });
|
|
47
|
+
return { action: unchanged, outcome, changed: false };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// NOT_COMMITTED deliberately returns to PROPOSED (not AUTHORIZED): the next
|
|
51
|
+
// execution must produce fresh authorization under the current policy epoch.
|
|
52
|
+
const to = outcome === "COMMITTED" ? "COMMITTED" : "PROPOSED";
|
|
53
|
+
const next = await transitionAction(target, { packageRoot, taskId, actionId, to,
|
|
54
|
+
expectedRevision: action.revision,
|
|
55
|
+
details: {
|
|
56
|
+
reconciliationOutcome: outcome,
|
|
57
|
+
evidenceRefs,
|
|
58
|
+
observedAt,
|
|
59
|
+
reconciliationAt: observedAt,
|
|
60
|
+
...(settlement ? {
|
|
61
|
+
authorityKind: settlement.authorityKind,
|
|
62
|
+
authorityRef: settlement.authorityRef,
|
|
63
|
+
} : {}),
|
|
64
|
+
} });
|
|
65
|
+
return { action: next, outcome, changed: true };
|
|
66
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { readAction, transitionVerifiedAction } from "./actions.js";
|
|
2
|
+
import { readExecutionArtifact } from "./execution.js";
|
|
3
|
+
import {
|
|
4
|
+
E_ACTION_STATE_MISMATCH,
|
|
5
|
+
E_ACTION_VERIFICATION_INVALID,
|
|
6
|
+
} from "./error-codes.js";
|
|
7
|
+
|
|
8
|
+
function verificationError(code, message) {
|
|
9
|
+
const error = new Error(message);
|
|
10
|
+
error.code = code;
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve canonical verification evidence for an action. For the first
|
|
16
|
+
* hardened release only ForgeLoop-validated execution artifacts qualify:
|
|
17
|
+
* an independent passed command execution bound to this task. Arbitrary
|
|
18
|
+
* strings and actor-reported observations fail closed (INV-VERIFY-01).
|
|
19
|
+
*/
|
|
20
|
+
async function resolveActionVerificationEvidence({
|
|
21
|
+
target,
|
|
22
|
+
packageRoot,
|
|
23
|
+
taskId,
|
|
24
|
+
action,
|
|
25
|
+
evidenceRef,
|
|
26
|
+
}) {
|
|
27
|
+
if (typeof evidenceRef !== "string" || !evidenceRef || evidenceRef.length > 256) {
|
|
28
|
+
throw verificationError(E_ACTION_VERIFICATION_INVALID, "verification evidenceRef must be a bounded non-empty string");
|
|
29
|
+
}
|
|
30
|
+
let artifact;
|
|
31
|
+
try {
|
|
32
|
+
artifact = await readExecutionArtifact({
|
|
33
|
+
target,
|
|
34
|
+
packageRoot,
|
|
35
|
+
taskId,
|
|
36
|
+
executionRef: evidenceRef,
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw verificationError(
|
|
40
|
+
E_ACTION_VERIFICATION_INVALID,
|
|
41
|
+
`verification evidence does not resolve to a canonical ForgeLoop execution artifact: ${error.message}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const execution = artifact.value ?? artifact;
|
|
45
|
+
if (execution.status !== "passed") {
|
|
46
|
+
throw verificationError(E_ACTION_VERIFICATION_INVALID, "action verification evidence must be a passed execution");
|
|
47
|
+
}
|
|
48
|
+
if (execution.taskId !== taskId) {
|
|
49
|
+
throw verificationError(E_ACTION_VERIFICATION_INVALID, "verification evidence belongs to a different task");
|
|
50
|
+
}
|
|
51
|
+
// The action's own commit execution proves local completion only; it can
|
|
52
|
+
// never double as independent postcondition verification.
|
|
53
|
+
if (execution.checkId === `action:${action.actionId}`) {
|
|
54
|
+
throw verificationError(
|
|
55
|
+
E_ACTION_VERIFICATION_INVALID,
|
|
56
|
+
"action execution is commit evidence, not independent postcondition verification",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
// Verification evidence must cover the exact immutable action requirement;
|
|
60
|
+
// a passed check for any other requirement is not postcondition evidence
|
|
61
|
+
// (INV-FINAL-VERIFY-01).
|
|
62
|
+
if (typeof action.requirement !== "string" || action.requirement.length === 0) {
|
|
63
|
+
throw verificationError(
|
|
64
|
+
E_ACTION_VERIFICATION_INVALID,
|
|
65
|
+
`action ${action.actionId} has no canonical requirement to verify`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (execution.requirement !== action.requirement) {
|
|
69
|
+
throw verificationError(
|
|
70
|
+
E_ACTION_VERIFICATION_INVALID,
|
|
71
|
+
`verification evidence covers requirement ${JSON.stringify(execution.requirement)} instead of ${JSON.stringify(action.requirement)}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return { kind: "FORGELOOP_EXECUTION", evidenceRef };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Canonical action verification (INV-VERIFY-01). VERIFIED is not a
|
|
79
|
+
* caller-reportable status: it requires a validated independent canonical
|
|
80
|
+
* evidence reference produced by e.g. run-check.
|
|
81
|
+
*/
|
|
82
|
+
export async function verifyAction({
|
|
83
|
+
target,
|
|
84
|
+
packageRoot,
|
|
85
|
+
taskId,
|
|
86
|
+
actionId,
|
|
87
|
+
evidenceRef,
|
|
88
|
+
}) {
|
|
89
|
+
const action = await readAction(target, { packageRoot, taskId, actionId });
|
|
90
|
+
if (action.state !== "COMMITTED") {
|
|
91
|
+
throw verificationError(
|
|
92
|
+
E_ACTION_STATE_MISMATCH,
|
|
93
|
+
`action ${actionId} must be COMMITTED before verification; current state is ${action.state}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const evidence = await resolveActionVerificationEvidence({
|
|
97
|
+
target, packageRoot, taskId, action, evidenceRef,
|
|
98
|
+
});
|
|
99
|
+
return transitionVerifiedAction(target, {
|
|
100
|
+
packageRoot,
|
|
101
|
+
taskId,
|
|
102
|
+
actionId: action.actionId,
|
|
103
|
+
expectedRevision: action.revision,
|
|
104
|
+
expectedFingerprint: action.actionFingerprint,
|
|
105
|
+
details: {
|
|
106
|
+
evidenceRef: evidence.evidenceRef,
|
|
107
|
+
evidenceKind: evidence.kind,
|
|
108
|
+
verifiedAt: new Date().toISOString(),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|