@cassiomc1/forgeloop 1.13.0 → 1.14.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/AGENT_COMPATIBILITY.md +8 -0
- package/DOCS_INDEX.md +35 -3
- package/ENG/nodejs-backend-development-eng.md +2 -2
- package/ENG/sec-code-eng.md +7 -7
- package/EXECUTION_STATE.md +12 -0
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/README.md +40 -25
- package/TERMINOLOGY.md +2 -0
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +19 -1
- package/completions/forgeloop.bash +37 -1
- package/completions/forgeloop.fish +123 -1
- package/docs/ADVISORY_CONTEXT.md +25 -0
- package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
- package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +27 -2
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +123 -0
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +366 -6
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +32 -11
- package/docs/JEV_BENCHMARKS.md +31 -0
- package/docs/MODEL_ROUTING.md +37 -0
- package/docs/OPENSRC_ADAPTER.md +241 -0
- package/docs/PACKAGE_CONTENTS.md +35 -8
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +9 -0
- package/docs/RELEASE_CHECKLIST.md +38 -5
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
- package/docs/TEST_INTELLIGENCE.md +29 -0
- package/docs/TEST_PRUNING.md +14 -0
- package/docs/TROUBLESHOOTING.md +198 -1
- package/docs/UNIVERSAL_INTEGRATION.md +31 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
- package/docs/diagrams/README.md +13 -9
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/docs/documentation-manifest.json +750 -5
- package/docs/protocol-requirements.json +24 -0
- package/package.json +30 -3
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +6 -6
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +4 -1
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/skills/forgeloop/README.md +9 -0
- package/skills/forgeloop/SKILL.md +77 -0
- package/skills/forgeloop/references/lifecycle.md +9 -0
- package/skills/forgeloop/references/recovery.md +7 -0
- package/skills/forgeloop/references/verification.md +7 -0
- package/src/adapters/agent-browser/assertions.js +47 -0
- package/src/adapters/agent-browser/commands.js +54 -0
- package/src/adapters/agent-browser/index.js +3 -0
- package/src/adapters/agent-browser/locator.js +40 -0
- package/src/adapters/agent-browser/process.js +215 -0
- package/src/adapters/agent-browser/provider.js +313 -0
- package/src/adapters/emulated-services/constants.js +24 -0
- package/src/adapters/emulated-services/index.js +7 -0
- package/src/adapters/emulated-services/process.js +162 -0
- package/src/adapters/emulated-services/provider.js +282 -0
- package/src/adapters/opensrc/normalize.js +90 -0
- package/src/adapters/opensrc/process.js +248 -0
- package/src/adapters/opensrc/provider.js +338 -0
- package/src/adapters/opensrc/search.js +264 -0
- package/src/adapters/typesafe/client.js +28 -0
- package/src/adapters/typesafe/engine.js +63 -0
- package/src/adapters/typesafe/normalize.js +41 -0
- package/src/cli.js +108 -0
- package/src/commands/checkpoint-revalidate.js +176 -0
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +264 -0
- package/src/commands/contract-revise.js +236 -0
- package/src/commands/decision-show.js +14 -0
- package/src/commands/decision-status.js +22 -0
- package/src/commands/discover.js +41 -0
- package/src/commands/doctor.js +15 -0
- package/src/commands/gate-record.js +205 -0
- package/src/commands/gate-revalidate.js +137 -0
- package/src/commands/model-route.js +32 -0
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +17 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/test-inventory.js +5 -0
- package/src/commands/test-prune-plan.js +5 -0
- package/src/commands/test-prune-probe.js +5 -0
- package/src/commands/test-utility.js +5 -0
- package/src/commands/validate-protocol.js +10 -1
- package/src/core/artifact-registry.js +24 -0
- package/src/core/audit-ux.js +514 -0
- package/src/core/browser-verification/constants.js +149 -0
- package/src/core/browser-verification/normalize.js +254 -0
- package/src/core/browser-verification/provider.js +519 -0
- package/src/core/browser-verification/service.js +115 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +241 -0
- package/src/core/command-executors.js +110 -0
- package/src/core/command-input.js +115 -43
- package/src/core/completion-artifacts.js +14 -5
- package/src/core/completion.js +4 -6
- package/src/core/config.js +3 -0
- package/src/core/context-compiler/budget.js +9 -0
- package/src/core/context-compiler/candidates.js +39 -0
- package/src/core/context-compiler/compiler.js +63 -0
- package/src/core/context-compiler/fingerprint.js +11 -0
- package/src/core/context-compiler/policy.js +13 -0
- package/src/core/context-compiler/result.js +23 -0
- package/src/core/contract-bootstrap-recovery.js +655 -0
- package/src/core/contract-revision.js +210 -0
- package/src/core/decision/artifact.js +69 -0
- package/src/core/decision/benchmarks.js +103 -0
- package/src/core/decision/cache.js +27 -0
- package/src/core/decision/constants.js +58 -0
- package/src/core/decision/cutover.js +34 -0
- package/src/core/decision/engine.js +22 -0
- package/src/core/decision/errors.js +68 -0
- package/src/core/decision/events.js +101 -0
- package/src/core/decision/freshness.js +19 -0
- package/src/core/decision/normalizers/index.js +115 -0
- package/src/core/decision/policy.js +18 -0
- package/src/core/decision/projection.js +16 -0
- package/src/core/decision/question-registry.js +201 -0
- package/src/core/decision/request.js +26 -0
- package/src/core/decision/resolver.js +130 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +156 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +30 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/error-codes.js +278 -0
- package/src/core/events.js +226 -57
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +14 -0
- package/src/core/execution-profile.js +63 -38
- package/src/core/gate-provenance.js +124 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/model-router/constants.js +10 -0
- package/src/core/model-router/policy.js +103 -0
- package/src/core/model-router/router.js +37 -0
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +130 -42
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +7 -2
- package/src/core/next-action.js +35 -7
- package/src/core/phase.js +128 -10
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +128 -52
- package/src/core/recovery-history.js +1 -0
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +15 -1
- package/src/core/router.js +67 -1
- package/src/core/runtime-context.js +118 -61
- package/src/core/schema-validation.js +3 -0
- package/src/core/security-review/constants.js +64 -0
- package/src/core/security-review/normalize.js +245 -0
- package/src/core/security-review/provider.js +204 -0
- package/src/core/security-review/service.js +134 -0
- package/src/core/semantic-planning/constants.js +19 -0
- package/src/core/semantic-planning/projection.js +94 -0
- package/src/core/semantic-planning/service.js +15 -0
- package/src/core/sources.js +37 -0
- package/src/core/task-claim-state.js +201 -1
- package/src/core/task-conflict-inspection.js +31 -5
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/templates.js +3 -0
- package/src/core/test-intelligence/benchmarks.js +68 -0
- package/src/core/test-intelligence/inventory.js +73 -0
- package/src/core/test-intelligence/prune.js +90 -0
- package/src/core/test-intelligence/semantic-state.js +15 -0
- package/src/core/test-intelligence/service.js +40 -0
- package/src/core/test-intelligence/utility.js +50 -0
- package/src/core/trace.js +11 -7
- package/src/core/transaction.js +1 -0
- package/src/integration.d.ts +492 -0
- package/src/integration.js +54 -0
- package/src/providers/README.md +47 -0
- package/src/providers/capabilities.js +46 -0
- package/src/providers/errors.js +15 -0
- package/src/providers/index.js +29 -0
- package/src/providers/json-snapshot.js +105 -0
- package/src/providers/registry.js +152 -0
package/src/core/events.js
CHANGED
|
@@ -9,6 +9,15 @@ import { assertSchema, readSchema } from "./schema-validation.js";
|
|
|
9
9
|
import { assertSecretFree } from "./receipt.js";
|
|
10
10
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
11
11
|
import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
|
|
12
|
+
import {
|
|
13
|
+
CONTRACT_BOOTSTRAP_REPAIR_EVENT,
|
|
14
|
+
ROUTE_CHECKPOINT_BOUND_EVENT,
|
|
15
|
+
assertContractBootstrapRepairDetails,
|
|
16
|
+
assertContractBootstrapRepairMigrationDetails,
|
|
17
|
+
isLegacyContractBootstrapRepairMarkerShape,
|
|
18
|
+
validateContractBootstrapRepairMigrations,
|
|
19
|
+
repairMarkerErrors,
|
|
20
|
+
} from "./contract-bootstrap-recovery.js";
|
|
12
21
|
|
|
13
22
|
import { taskArtifactPath } from "./task-paths.js";
|
|
14
23
|
import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
|
|
@@ -33,6 +42,32 @@ import {
|
|
|
33
42
|
isLegacyRecoveryEventShape,
|
|
34
43
|
legacyRecoveryMigrationId,
|
|
35
44
|
} from "./task-recovery-migration.js";
|
|
45
|
+
import {
|
|
46
|
+
CHECKPOINT_REVALIDATED_EVENT,
|
|
47
|
+
assertCheckpointRevalidatedDetails,
|
|
48
|
+
validateCheckpointRevalidationCurrentBinding,
|
|
49
|
+
validateCheckpointRevalidationEventBindings,
|
|
50
|
+
} from "./checkpoint-revalidation.js";
|
|
51
|
+
import {
|
|
52
|
+
CONTRACT_REVISED_EVENT,
|
|
53
|
+
assertContractRevisedDetails,
|
|
54
|
+
validateContractRevisionCurrentBinding,
|
|
55
|
+
validateContractRevisionEventBindings,
|
|
56
|
+
} from "./contract-revision.js";
|
|
57
|
+
import {
|
|
58
|
+
GATE_SATISFIED_EVENT,
|
|
59
|
+
GATE_REVALIDATED_EVENT,
|
|
60
|
+
assertGateSatisfiedDetails,
|
|
61
|
+
assertGateRevalidatedDetails,
|
|
62
|
+
hasCurrentGateEvidence,
|
|
63
|
+
validateGateSatisfactionBindings,
|
|
64
|
+
} from "./gate-provenance.js";
|
|
65
|
+
import {
|
|
66
|
+
assertSemanticDecisionDetails,
|
|
67
|
+
validateSemanticDecisionArtifactBindings,
|
|
68
|
+
validateSemanticDecisionEventBindings,
|
|
69
|
+
} from "./decision/events.js";
|
|
70
|
+
import { SEMANTIC_DECISION_RECORDED_EVENT, SEMANTIC_DECISION_SUPERSEDED_EVENT } from "./decision/constants.js";
|
|
36
71
|
|
|
37
72
|
const EVENT_SCHEMA_VERSION = 1;
|
|
38
73
|
export const LIFECYCLE_MILESTONES = Object.freeze([
|
|
@@ -60,6 +95,48 @@ const REPEATABLE_MILESTONES = new Set([
|
|
|
60
95
|
"PREFLIGHT_READY",
|
|
61
96
|
]);
|
|
62
97
|
|
|
98
|
+
export function isRevisionEpochPlanRepeat(events = [], index = 0, event = null) {
|
|
99
|
+
if (event?.event !== "PLAN_RECORDED") return false;
|
|
100
|
+
const previousPlan = events.slice(0, index).findLast((candidate) => candidate.event === "PLAN_RECORDED");
|
|
101
|
+
const revision = events.slice(0, index).findLast((candidate) => candidate.event === CONTRACT_REVISED_EVENT);
|
|
102
|
+
return Boolean(previousPlan && revision && previousPlan.seq < revision.seq);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validateMilestoneChronology(events, index, event, milestoneCounts, lastMilestone, errors) {
|
|
106
|
+
const milestoneIndex = LIFECYCLE_MILESTONES.indexOf(event.event);
|
|
107
|
+
if (milestoneIndex < 0) return lastMilestone;
|
|
108
|
+
const count = (milestoneCounts.get(event.event) ?? 0) + 1;
|
|
109
|
+
milestoneCounts.set(event.event, count);
|
|
110
|
+
const revisionPlanRepeat = isRevisionEpochPlanRepeat(events, index, event);
|
|
111
|
+
if (count > 1 && !REPEATABLE_MILESTONES.has(event.event) && !revisionPlanRepeat) {
|
|
112
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
|
|
113
|
+
}
|
|
114
|
+
if (milestoneIndex > lastMilestone + 1) {
|
|
115
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `${event.event} is missing prerequisite milestone: ${LIFECYCLE_MILESTONES[lastMilestone + 1]}` });
|
|
116
|
+
} else if (milestoneIndex < lastMilestone && event.event !== "VERIFICATION_STARTED" && event.event !== "PREFLIGHT_READY") {
|
|
117
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `${event.event} is out of lifecycle order` });
|
|
118
|
+
} else if (milestoneIndex === lastMilestone && !REPEATABLE_MILESTONES.has(event.event) && !revisionPlanRepeat) {
|
|
119
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
|
|
120
|
+
}
|
|
121
|
+
if (milestoneIndex > lastMilestone) return milestoneIndex;
|
|
122
|
+
return lastMilestone;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validateExecutionGateChronology(events, index, seen, errors) {
|
|
126
|
+
if (!seen.has("ROUTE_VALIDATED")) errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before route validation" });
|
|
127
|
+
if (!seen.has("CONTRACT_VALIDATED")) errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before contract validation" });
|
|
128
|
+
if (!seen.has("PREFLIGHT_READY")) errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before preflight readiness" });
|
|
129
|
+
const preflight = events.slice(0, index).findLast((candidate) => candidate.event === "PREFLIGHT_READY");
|
|
130
|
+
const requiredGates = preflight?.details?.requiredGates ?? [];
|
|
131
|
+
const latestContractRevisionSeq = events.slice(0, index).findLast((candidate) => candidate.event === CONTRACT_REVISED_EVENT)?.seq ?? 0;
|
|
132
|
+
const beforeExecution = events.slice(0, index);
|
|
133
|
+
for (const gate of requiredGates) {
|
|
134
|
+
if (!hasCurrentGateEvidence(beforeExecution, events[index]?.taskId, gate, latestContractRevisionSeq)) {
|
|
135
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `execution started before gate satisfaction: ${gate}` });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
63
140
|
function eventIndexPath(eventsPath) {
|
|
64
141
|
return `${eventsPath}.index.json`;
|
|
65
142
|
}
|
|
@@ -125,6 +202,22 @@ export function validateKnownEventDetails(event) {
|
|
|
125
202
|
case "CHECKPOINT_RECONCILED":
|
|
126
203
|
assertReconcileClosureDetails(event.details);
|
|
127
204
|
return;
|
|
205
|
+
case CHECKPOINT_REVALIDATED_EVENT:
|
|
206
|
+
assertCheckpointRevalidatedDetails(event.details);
|
|
207
|
+
return;
|
|
208
|
+
case CONTRACT_REVISED_EVENT:
|
|
209
|
+
assertContractRevisedDetails(event.details);
|
|
210
|
+
return;
|
|
211
|
+
case GATE_SATISFIED_EVENT:
|
|
212
|
+
assertGateSatisfiedDetails(event.details);
|
|
213
|
+
return;
|
|
214
|
+
case GATE_REVALIDATED_EVENT:
|
|
215
|
+
assertGateRevalidatedDetails(event.details);
|
|
216
|
+
return;
|
|
217
|
+
case SEMANTIC_DECISION_RECORDED_EVENT:
|
|
218
|
+
case SEMANTIC_DECISION_SUPERSEDED_EVENT:
|
|
219
|
+
assertSemanticDecisionDetails(event.details, event.event);
|
|
220
|
+
return;
|
|
128
221
|
case "TASK_RECOVERY_RECORDED":
|
|
129
222
|
assertRecoveryRecordedDetails(event.details);
|
|
130
223
|
return;
|
|
@@ -135,18 +228,27 @@ export function validateKnownEventDetails(event) {
|
|
|
135
228
|
if (!event.details?.recoveryId && isLegacyRecoveryDetailsShape(event.details)) return;
|
|
136
229
|
assertRecoveryRecordedDetails(event.details);
|
|
137
230
|
return;
|
|
231
|
+
case "TASK_ABANDONED":
|
|
232
|
+
assertTaskAbandonedDetails(event.details);
|
|
233
|
+
return;
|
|
138
234
|
case "LEGACY_RECOVERY_MIGRATION_RECORDED":
|
|
139
235
|
assertLegacyMigrationDetails(event.details);
|
|
140
236
|
return;
|
|
237
|
+
case CONTRACT_BOOTSTRAP_REPAIR_EVENT:
|
|
238
|
+
case "CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_RECORDED":
|
|
239
|
+
validateKnownContractBootstrapEvent(event);
|
|
240
|
+
return;
|
|
241
|
+
case "ROUTE_REBOUND":
|
|
242
|
+
assertRouteReboundDetails(event.details);
|
|
243
|
+
return;
|
|
244
|
+
case ROUTE_CHECKPOINT_BOUND_EVENT:
|
|
245
|
+
assertRouteCheckpointBoundDetails(event.details);
|
|
246
|
+
return;
|
|
141
247
|
case "TASK_RECOVERY_RESUMED":
|
|
142
248
|
assertRecoveryResumedDetails(event.details);
|
|
143
249
|
return;
|
|
144
250
|
case "TRAJECTORY_EVALUATED":
|
|
145
|
-
|
|
146
|
-
|| typeof event.details.scenarioId !== "string" || !/^[a-f0-9]{64}$/.test(event.details.evaluationFingerprint ?? "")
|
|
147
|
-
|| event.fingerprint !== event.details.evaluationFingerprint) {
|
|
148
|
-
throw protocolError("E_EVENT_INVALID", "TRAJECTORY_EVALUATED requires a bound evaluationId, scenarioId, and fingerprint");
|
|
149
|
-
}
|
|
251
|
+
assertTrajectoryEvaluatedDetails(event);
|
|
150
252
|
return;
|
|
151
253
|
case "WORKSPACE_BOUND":
|
|
152
254
|
assertStructuredArtifactEvent(event, ["workspaceFingerprint"], "WORKSPACE_BOUND");
|
|
@@ -161,14 +263,7 @@ export function validateKnownEventDetails(event) {
|
|
|
161
263
|
assertFingerprint(event.details.digest, "HANDOFF_CREATED details.digest");
|
|
162
264
|
return;
|
|
163
265
|
case "HANDOFF_ACCEPTED":
|
|
164
|
-
|
|
165
|
-
|| typeof event.details.handoffId !== "string" || !/^handoff-[A-Za-z0-9_-]+$/.test(event.details.handoffId)
|
|
166
|
-
|| typeof event.details.handoffDigest !== "string"
|
|
167
|
-
|| typeof event.details.consumerId !== "string" || !event.details.consumerId.trim()
|
|
168
|
-
|| (event.details.harness !== undefined && (typeof event.details.harness !== "string" || !event.details.harness.trim()))) {
|
|
169
|
-
throw protocolError("E_EVENT_INVALID", "HANDOFF_ACCEPTED requires a valid handoffId, handoffDigest, and consumerId");
|
|
170
|
-
}
|
|
171
|
-
assertFingerprint(event.details.handoffDigest, "HANDOFF_ACCEPTED details.handoffDigest");
|
|
266
|
+
assertHandoffAcceptedDetails(event);
|
|
172
267
|
return;
|
|
173
268
|
case "RESPONSIBILITY_SET":
|
|
174
269
|
assertStructuredArtifactEvent(event, ["responsibilityFingerprint"], "RESPONSIBILITY_SET");
|
|
@@ -207,6 +302,34 @@ export function validateKnownEventDetails(event) {
|
|
|
207
302
|
}
|
|
208
303
|
}
|
|
209
304
|
|
|
305
|
+
function validateKnownContractBootstrapEvent(event) {
|
|
306
|
+
if (event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT) {
|
|
307
|
+
if (isLegacyContractBootstrapRepairMarkerShape(event)) return;
|
|
308
|
+
assertContractBootstrapRepairDetails(event.details);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
assertContractBootstrapRepairMigrationDetails(event.details);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function assertTrajectoryEvaluatedDetails(event) {
|
|
315
|
+
if (!event.details || !/^eval-[A-Za-z0-9_-]+$/.test(event.details.evaluationId ?? "")
|
|
316
|
+
|| typeof event.details.scenarioId !== "string" || !/^[a-f0-9]{64}$/.test(event.details.evaluationFingerprint ?? "")
|
|
317
|
+
|| event.fingerprint !== event.details.evaluationFingerprint) {
|
|
318
|
+
throw protocolError("E_EVENT_INVALID", "TRAJECTORY_EVALUATED requires a bound evaluationId, scenarioId, and fingerprint");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function assertHandoffAcceptedDetails(event) {
|
|
323
|
+
if (!event.details || typeof event.details !== "object" || Array.isArray(event.details)
|
|
324
|
+
|| typeof event.details.handoffId !== "string" || !/^handoff-[A-Za-z0-9_-]+$/.test(event.details.handoffId)
|
|
325
|
+
|| typeof event.details.handoffDigest !== "string"
|
|
326
|
+
|| typeof event.details.consumerId !== "string" || !event.details.consumerId.trim()
|
|
327
|
+
|| (event.details.harness !== undefined && (typeof event.details.harness !== "string" || !event.details.harness.trim()))) {
|
|
328
|
+
throw protocolError("E_EVENT_INVALID", "HANDOFF_ACCEPTED requires a valid handoffId, handoffDigest, and consumerId");
|
|
329
|
+
}
|
|
330
|
+
assertFingerprint(event.details.handoffDigest, "HANDOFF_ACCEPTED details.handoffDigest");
|
|
331
|
+
}
|
|
332
|
+
|
|
210
333
|
function assertStringList(value, label) {
|
|
211
334
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item)) {
|
|
212
335
|
throw protocolError("E_EVENT_INVALID", `${label} must be an array of non-empty strings`);
|
|
@@ -234,6 +357,35 @@ function assertStructuredArtifactEvent(event, requiredDetails, label) {
|
|
|
234
357
|
}
|
|
235
358
|
}
|
|
236
359
|
|
|
360
|
+
function assertRouteReboundDetails(details) {
|
|
361
|
+
const keys = ["routeFingerprint", "contractFingerprint", "previousRouteFingerprint"];
|
|
362
|
+
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
363
|
+
|| Object.keys(details).length !== keys.length
|
|
364
|
+
|| keys.some((key) => !Object.prototype.hasOwnProperty.call(details, key))) {
|
|
365
|
+
throw protocolError("E_EVENT_INVALID", "ROUTE_REBOUND requires exactly route, contract, and previous route fingerprints");
|
|
366
|
+
}
|
|
367
|
+
assertFingerprint(details.routeFingerprint, "ROUTE_REBOUND details.routeFingerprint");
|
|
368
|
+
assertFingerprint(details.contractFingerprint, "ROUTE_REBOUND details.contractFingerprint");
|
|
369
|
+
if (details.previousRouteFingerprint !== null) {
|
|
370
|
+
assertFingerprint(details.previousRouteFingerprint, "ROUTE_REBOUND details.previousRouteFingerprint");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function assertRouteCheckpointBoundDetails(details) {
|
|
375
|
+
const keys = ["routeFingerprint", "contractFingerprint", "selectedGuides"];
|
|
376
|
+
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
377
|
+
|| Object.keys(details).length !== keys.length
|
|
378
|
+
|| keys.some((key) => !Object.prototype.hasOwnProperty.call(details, key))) {
|
|
379
|
+
throw protocolError("E_EVENT_INVALID", "ROUTE_CHECKPOINT_BOUND requires exactly route, contract, and selected guide identity");
|
|
380
|
+
}
|
|
381
|
+
assertFingerprint(details.routeFingerprint, "ROUTE_CHECKPOINT_BOUND details.routeFingerprint");
|
|
382
|
+
assertFingerprint(details.contractFingerprint, "ROUTE_CHECKPOINT_BOUND details.contractFingerprint");
|
|
383
|
+
assertStringList(details.selectedGuides, "ROUTE_CHECKPOINT_BOUND details.selectedGuides");
|
|
384
|
+
if (new Set(details.selectedGuides).size !== details.selectedGuides.length) {
|
|
385
|
+
throw protocolError("E_EVENT_INVALID", "ROUTE_CHECKPOINT_BOUND details.selectedGuides must not contain duplicates");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
237
389
|
function assertRecoveryRecordedDetails(details) {
|
|
238
390
|
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
239
391
|
throw protocolError("E_EVENT_INVALID", "recovery event requires structured details");
|
|
@@ -256,6 +408,44 @@ function assertRecoveryRecordedDetails(details) {
|
|
|
256
408
|
assertStringList(details.releasedClaims, "recovery event details.releasedClaims");
|
|
257
409
|
}
|
|
258
410
|
|
|
411
|
+
function assertTaskAbandonedDetails(details) {
|
|
412
|
+
const keys = [
|
|
413
|
+
"recoveryId", "classification", "reasonCodes", "previousPhase", "previousRevision",
|
|
414
|
+
"previousHead", "previousBranch", "currentHead", "currentBranch",
|
|
415
|
+
"releasedClaims", "authorityKind",
|
|
416
|
+
];
|
|
417
|
+
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
418
|
+
|| Object.keys(details).length !== keys.length
|
|
419
|
+
|| keys.some((key) => !Object.prototype.hasOwnProperty.call(details, key))) {
|
|
420
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED requires the exact abandonment boundary details");
|
|
421
|
+
}
|
|
422
|
+
if (typeof details.recoveryId !== "string" || details.recoveryId.length === 0) {
|
|
423
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.recoveryId must be a non-empty string");
|
|
424
|
+
}
|
|
425
|
+
if (details.classification !== "ABANDONED") {
|
|
426
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.classification must be ABANDONED");
|
|
427
|
+
}
|
|
428
|
+
assertStringList(details.reasonCodes, "TASK_ABANDONED details.reasonCodes");
|
|
429
|
+
if (details.reasonCodes.length !== 1 || details.reasonCodes[0] !== "CALLER_ABANDONED") {
|
|
430
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.reasonCodes must be [CALLER_ABANDONED]");
|
|
431
|
+
}
|
|
432
|
+
if (typeof details.previousPhase !== "string" || details.previousPhase.length === 0) {
|
|
433
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.previousPhase must be a non-empty string");
|
|
434
|
+
}
|
|
435
|
+
if (!Number.isInteger(details.previousRevision) || details.previousRevision < 0) {
|
|
436
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.previousRevision must be a non-negative integer");
|
|
437
|
+
}
|
|
438
|
+
for (const key of ["previousHead", "previousBranch", "currentHead", "currentBranch"]) {
|
|
439
|
+
if (typeof details[key] !== "string" && details[key] !== null) {
|
|
440
|
+
throw protocolError("E_EVENT_INVALID", `TASK_ABANDONED details.${key} must be a string or null`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
assertStringList(details.releasedClaims, "TASK_ABANDONED details.releasedClaims");
|
|
444
|
+
if (details.authorityKind !== "CALLER_ACKNOWLEDGED") {
|
|
445
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.authorityKind must be CALLER_ACKNOWLEDGED");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
259
449
|
function assertRecoveryResumedDetails(details) {
|
|
260
450
|
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
261
451
|
|| typeof details.recoveryId !== "string" || !details.recoveryId) {
|
|
@@ -542,50 +732,10 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
|
|
|
542
732
|
} catch (err) {
|
|
543
733
|
errors.push({ code: err.code ?? "E_EVENT_INVALID", message: `event ${event.seq} (${event.event}): ${err.message}` });
|
|
544
734
|
}
|
|
545
|
-
|
|
546
|
-
if (milestoneIndex >= 0) {
|
|
547
|
-
const count = (milestoneCounts.get(event.event) ?? 0) + 1;
|
|
548
|
-
milestoneCounts.set(event.event, count);
|
|
549
|
-
if (count > 1 && !REPEATABLE_MILESTONES.has(event.event)) {
|
|
550
|
-
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
|
|
551
|
-
}
|
|
552
|
-
if (milestoneIndex > lastMilestone + 1) {
|
|
553
|
-
errors.push({
|
|
554
|
-
code: "E_PHASE_CHRONOLOGY_INVALID",
|
|
555
|
-
message: `${event.event} is missing prerequisite milestone: ${LIFECYCLE_MILESTONES[lastMilestone + 1]}`,
|
|
556
|
-
});
|
|
557
|
-
} else if (milestoneIndex < lastMilestone && event.event !== "VERIFICATION_STARTED"
|
|
558
|
-
&& event.event !== "PREFLIGHT_READY") {
|
|
559
|
-
// VERIFICATION_STARTED re-enters per verification cycle; PREFLIGHT_READY
|
|
560
|
-
// may be refreshed mid-lifecycle (policy/contract evolution) after its
|
|
561
|
-
// prerequisites were already satisfied by the earlier occurrence.
|
|
562
|
-
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `${event.event} is out of lifecycle order` });
|
|
563
|
-
} else if (milestoneIndex === lastMilestone && !REPEATABLE_MILESTONES.has(event.event)) {
|
|
564
|
-
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
|
|
565
|
-
}
|
|
566
|
-
if (milestoneIndex > lastMilestone) lastMilestone = milestoneIndex;
|
|
567
|
-
}
|
|
735
|
+
lastMilestone = validateMilestoneChronology(events, index, event, milestoneCounts, lastMilestone, errors);
|
|
568
736
|
seen.add(event.event);
|
|
569
|
-
if (event.event === "EXECUTION_STARTED" && !seen.has("ROUTE_VALIDATED")) {
|
|
570
|
-
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before route validation" });
|
|
571
|
-
}
|
|
572
|
-
if (event.event === "EXECUTION_STARTED" && !seen.has("CONTRACT_VALIDATED")) {
|
|
573
|
-
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before contract validation" });
|
|
574
|
-
}
|
|
575
|
-
if (event.event === "EXECUTION_STARTED" && !seen.has("PREFLIGHT_READY")) {
|
|
576
|
-
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before preflight readiness" });
|
|
577
|
-
}
|
|
578
737
|
if (event.event === "EXECUTION_STARTED") {
|
|
579
|
-
|
|
580
|
-
const requiredGates = preflight?.details?.requiredGates ?? [];
|
|
581
|
-
const satisfiedGates = new Set(events.slice(0, index)
|
|
582
|
-
.filter((candidate) => candidate.event === "GATE_SATISFIED")
|
|
583
|
-
.map((candidate) => candidate.details?.gate));
|
|
584
|
-
for (const gate of requiredGates) {
|
|
585
|
-
if (!satisfiedGates.has(gate)) {
|
|
586
|
-
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `execution started before gate satisfaction: ${gate}` });
|
|
587
|
-
}
|
|
588
|
-
}
|
|
738
|
+
validateExecutionGateChronology(events, index, seen, errors);
|
|
589
739
|
}
|
|
590
740
|
if (event.event === "VERIFICATION_RECORDED" && !seen.has("VERIFICATION_STARTED")) {
|
|
591
741
|
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "verification evidence recorded before verification started" });
|
|
@@ -620,11 +770,30 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
|
|
|
620
770
|
validateLegacyRecoveryMigrations(events, errors, {
|
|
621
771
|
allowUnmigratedLegacyRecoveryEvents: options?.allowUnmigratedLegacyRecoveryEvents === true,
|
|
622
772
|
});
|
|
623
|
-
|
|
773
|
+
errors.push(...validateCheckpointRevalidationEventBindings(events));
|
|
774
|
+
errors.push(...validateContractRevisionEventBindings(events));
|
|
775
|
+
errors.push(...validateGateSatisfactionBindings(events));
|
|
776
|
+
errors.push(...validateSemanticDecisionEventBindings(events));
|
|
777
|
+
errors.push(...await validateSemanticDecisionArtifactBindings(target, packageRoot, events));
|
|
778
|
+
const repairedErrors = validateContractBootstrapRepairLedger(events, errors, options);
|
|
779
|
+
return { valid: repairedErrors.length === 0, events, errors: repairedErrors };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function validateContractBootstrapRepairLedger(events, errors, options) {
|
|
783
|
+
const allowUnmigrated = options?.allowUnmigratedLegacyContractBootstrapRepairMarkers === true;
|
|
784
|
+
validateContractBootstrapRepairMigrations(events, errors, {
|
|
785
|
+
allowUnmigratedLegacyContractBootstrapRepairMarkers: allowUnmigrated,
|
|
786
|
+
});
|
|
787
|
+
return repairMarkerErrors(events, errors, {
|
|
788
|
+
allowUnmigratedLegacyContractBootstrapRepairMarkers: allowUnmigrated,
|
|
789
|
+
});
|
|
624
790
|
}
|
|
625
791
|
|
|
626
792
|
export function validateStateLedgerCoherence(state, events) {
|
|
627
|
-
const errors = [
|
|
793
|
+
const errors = [
|
|
794
|
+
...validateCheckpointRevalidationCurrentBinding(state, events),
|
|
795
|
+
...validateContractRevisionCurrentBinding(state, events),
|
|
796
|
+
];
|
|
628
797
|
if (!Number.isInteger(state.verificationCycle)) return errors;
|
|
629
798
|
const taskEvents = events.filter((event) => event.taskId === state.taskId);
|
|
630
799
|
const observed = new Set(taskEvents.map((event) => event.event));
|
|
@@ -164,6 +164,15 @@ export function terminalRequirements(requirements = []) {
|
|
|
164
164
|
return normalizeRequirements(requirements).filter((requirement) => requirement.terminalOwned);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
export function terminalRequirementsForContract(contractValue = {}, { additionalEvidence = [] } = {}) {
|
|
168
|
+
const pool = [
|
|
169
|
+
...(contractValue.verification ?? []),
|
|
170
|
+
...(contractValue.successCriteria ?? []),
|
|
171
|
+
...additionalEvidence,
|
|
172
|
+
];
|
|
173
|
+
return terminalRequirements(pool);
|
|
174
|
+
}
|
|
175
|
+
|
|
167
176
|
export function lifecycleRequirements(requirements = []) {
|
|
168
177
|
return normalizeRequirements(requirements).filter((requirement) => requirement.type === "LIFECYCLE");
|
|
169
178
|
}
|
|
@@ -6,6 +6,7 @@ import { readPersistedRoute } from "./route-artifact.js";
|
|
|
6
6
|
import { stateIdentityErrors } from "./completion-relationships.js";
|
|
7
7
|
import { classifyLoadedWorkState } from "./work-state.js";
|
|
8
8
|
import { taskArtifactPath } from "./task-paths.js";
|
|
9
|
+
import { hasCurrentGateEvidence } from "./gate-provenance.js";
|
|
9
10
|
|
|
10
11
|
const START_EXECUTION_EVENTS = Object.freeze([
|
|
11
12
|
"CONTRACT_VALIDATED",
|
|
@@ -129,6 +130,19 @@ function prerequisiteLedgerErrors(ledger, taskId, preflight, route) {
|
|
|
129
130
|
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.route],
|
|
130
131
|
));
|
|
131
132
|
}
|
|
133
|
+
const latestContractRevisionSeq = currentEvents.findLast((event) => event.event === "CONTRACT_REVISED")?.seq ?? 0;
|
|
134
|
+
const currentEpochSatisfiedGates = new Set(preflight.requiredGates.filter((gate) => (
|
|
135
|
+
hasCurrentGateEvidence(currentEvents, taskId, gate, latestContractRevisionSeq)
|
|
136
|
+
)));
|
|
137
|
+
for (const gate of preflight.requiredGates) {
|
|
138
|
+
if (!currentEpochSatisfiedGates.has(gate)) {
|
|
139
|
+
errors.push(issue(
|
|
140
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
141
|
+
`execution started before current-contract gate satisfaction: ${gate}`,
|
|
142
|
+
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.preflight],
|
|
143
|
+
));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
132
146
|
return errors;
|
|
133
147
|
}
|
|
134
148
|
|
|
@@ -162,12 +162,63 @@ export function projectExecutionProfile(route) {
|
|
|
162
162
|
return route.executionProfile?.resolved ?? LEGACY_EXECUTION_PROFILE;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function routeProfileSignals(normalizedRoute) {
|
|
166
|
+
const fullReasons = [];
|
|
167
|
+
const balancedReasons = [];
|
|
168
|
+
if (FULL_WORK_TYPES.has(normalizedRoute.workType)) fullReasons.push(`WORK_${normalizedRoute.workType.toUpperCase().replaceAll("-", "_")}`);
|
|
169
|
+
if (BALANCED_WORK_TYPES.has(normalizedRoute.workType)) balancedReasons.push(`WORK_${normalizedRoute.workType.toUpperCase().replaceAll("-", "_")}`);
|
|
170
|
+
for (const surface of normalizedRoute.surfaces) {
|
|
171
|
+
if (FULL_SURFACES.has(surface)) fullReasons.push(`SURFACE_${surface.toUpperCase().replaceAll("-", "_")}`);
|
|
172
|
+
else if (BALANCED_SURFACES.has(surface)) balancedReasons.push(`SURFACE_${surface.toUpperCase().replaceAll("-", "_")}`);
|
|
173
|
+
}
|
|
174
|
+
for (const risk of normalizedRoute.risks) {
|
|
175
|
+
if (FULL_RISKS.has(risk)) fullReasons.push(`RISK_${risk.toUpperCase().replaceAll("-", "_")}`);
|
|
176
|
+
else if (BALANCED_RISKS.has(risk)) balancedReasons.push(`RISK_${risk.toUpperCase().replaceAll("-", "_")}`);
|
|
177
|
+
}
|
|
178
|
+
if (normalizedRoute.behaviorChange) balancedReasons.push("WORK_BEHAVIOR_CHANGE");
|
|
179
|
+
if (normalizedRoute.executableChange) balancedReasons.push("WORK_EXECUTABLE_CHANGE");
|
|
180
|
+
return { fullReasons, balancedReasons };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function contractProfileSignals(textSignals, normalizedRoute) {
|
|
184
|
+
const fullReasons = [];
|
|
185
|
+
if (textSignals.secrets) fullReasons.push("RISK_SECRETS");
|
|
186
|
+
if (textSignals.personalData) fullReasons.push("RISK_PERSONAL_DATA");
|
|
187
|
+
if (textSignals.publication) fullReasons.push("RISK_PUBLICATION");
|
|
188
|
+
if (textSignals.destructive) fullReasons.push("RISK_DESTRUCTIVE");
|
|
189
|
+
if (textSignals.migration && normalizedRoute.surfaces.includes("database")) fullReasons.push("RISK_MIGRATION");
|
|
190
|
+
if (textSignals.payment) fullReasons.push("RISK_PAYMENT");
|
|
191
|
+
if (textSignals.externalMutation) fullReasons.push("RISK_EXTERNAL_MUTATION");
|
|
192
|
+
if (textSignals.authoritySensitiveExternalMutation) fullReasons.push("RISK_AUTHORITY_SENSITIVE_EXTERNAL_MUTATION");
|
|
193
|
+
if (textSignals.broadProductionValidation) fullReasons.push("RISK_PRODUCTION_VALIDATION");
|
|
194
|
+
return fullReasons;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function scopeProfileSignals(scope) {
|
|
198
|
+
const balancedReasons = [];
|
|
199
|
+
if (scope.multiDeliverable) balancedReasons.push("SCOPE_MULTI_DELIVERABLE");
|
|
200
|
+
if (scope.multiClaim) balancedReasons.push("SCOPE_MULTI_CLAIM");
|
|
201
|
+
if (scope.broad || scope.multiSurfaceExecutableChange) balancedReasons.push("SCOPE_BROAD");
|
|
202
|
+
return balancedReasons;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function profileSignals({ normalizedRoute, contract, taskDescriptor }) {
|
|
206
|
+
const route = routeProfileSignals(normalizedRoute);
|
|
207
|
+
const scope = scopeSignals({ routeInput: normalizedRoute, contract, taskDescriptor });
|
|
208
|
+
return {
|
|
209
|
+
fullReasons: [...route.fullReasons, ...contractProfileSignals(contractSignals(contract), normalizedRoute)],
|
|
210
|
+
balancedReasons: [...route.balancedReasons, ...scopeProfileSignals(scope)],
|
|
211
|
+
scope,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
165
215
|
export function resolveExecutionProfile({
|
|
166
216
|
routeInput = {},
|
|
167
217
|
contract = null,
|
|
168
218
|
taskDescriptor = null,
|
|
169
219
|
configuredProfile = "auto",
|
|
170
220
|
requestedProfile = null,
|
|
221
|
+
semanticRecommendation = null,
|
|
171
222
|
} = {}) {
|
|
172
223
|
const normalizedRoute = normalizedRouteInput(routeInput);
|
|
173
224
|
const configured = normalizeRequestedProfile(configuredProfile, "configuredProfile");
|
|
@@ -181,43 +232,7 @@ export function resolveExecutionProfile({
|
|
|
181
232
|
else reasons.push("PROFILE_DEFAULT_AUTO");
|
|
182
233
|
|
|
183
234
|
let floor = "light";
|
|
184
|
-
const fullReasons =
|
|
185
|
-
const balancedReasons = [];
|
|
186
|
-
const addFull = (reason) => fullReasons.push(reason);
|
|
187
|
-
const addBalanced = (reason) => balancedReasons.push(reason);
|
|
188
|
-
|
|
189
|
-
if (FULL_WORK_TYPES.has(normalizedRoute.workType)) {
|
|
190
|
-
addFull(`WORK_${normalizedRoute.workType.toUpperCase().replaceAll("-", "_")}`);
|
|
191
|
-
}
|
|
192
|
-
if (BALANCED_WORK_TYPES.has(normalizedRoute.workType)) {
|
|
193
|
-
addBalanced(`WORK_${normalizedRoute.workType.toUpperCase().replaceAll("-", "_")}`);
|
|
194
|
-
}
|
|
195
|
-
for (const surface of normalizedRoute.surfaces) {
|
|
196
|
-
if (FULL_SURFACES.has(surface)) addFull(`SURFACE_${surface.toUpperCase().replaceAll("-", "_")}`);
|
|
197
|
-
else if (BALANCED_SURFACES.has(surface)) addBalanced(`SURFACE_${surface.toUpperCase().replaceAll("-", "_")}`);
|
|
198
|
-
}
|
|
199
|
-
for (const risk of normalizedRoute.risks) {
|
|
200
|
-
if (FULL_RISKS.has(risk)) addFull(`RISK_${risk.toUpperCase().replaceAll("-", "_")}`);
|
|
201
|
-
else if (BALANCED_RISKS.has(risk)) addBalanced(`RISK_${risk.toUpperCase().replaceAll("-", "_")}`);
|
|
202
|
-
}
|
|
203
|
-
if (normalizedRoute.behaviorChange) addBalanced("WORK_BEHAVIOR_CHANGE");
|
|
204
|
-
if (normalizedRoute.executableChange) addBalanced("WORK_EXECUTABLE_CHANGE");
|
|
205
|
-
|
|
206
|
-
const textSignals = contractSignals(contract);
|
|
207
|
-
if (textSignals.secrets) addFull("RISK_SECRETS");
|
|
208
|
-
if (textSignals.personalData) addFull("RISK_PERSONAL_DATA");
|
|
209
|
-
if (textSignals.publication) addFull("RISK_PUBLICATION");
|
|
210
|
-
if (textSignals.destructive) addFull("RISK_DESTRUCTIVE");
|
|
211
|
-
if (textSignals.migration && normalizedRoute.surfaces.includes("database")) addFull("RISK_MIGRATION");
|
|
212
|
-
if (textSignals.payment) addFull("RISK_PAYMENT");
|
|
213
|
-
if (textSignals.externalMutation) addFull("RISK_EXTERNAL_MUTATION");
|
|
214
|
-
if (textSignals.authoritySensitiveExternalMutation) addFull("RISK_AUTHORITY_SENSITIVE_EXTERNAL_MUTATION");
|
|
215
|
-
if (textSignals.broadProductionValidation) addFull("RISK_PRODUCTION_VALIDATION");
|
|
216
|
-
|
|
217
|
-
const scope = scopeSignals({ routeInput: normalizedRoute, contract, taskDescriptor });
|
|
218
|
-
if (scope.multiDeliverable) addBalanced("SCOPE_MULTI_DELIVERABLE");
|
|
219
|
-
if (scope.multiClaim) addBalanced("SCOPE_MULTI_CLAIM");
|
|
220
|
-
if (scope.broad || scope.multiSurfaceExecutableChange) addBalanced("SCOPE_BROAD");
|
|
235
|
+
const { fullReasons, balancedReasons, scope } = profileSignals({ normalizedRoute, contract, taskDescriptor });
|
|
221
236
|
|
|
222
237
|
if (fullReasons.length > 0) {
|
|
223
238
|
floor = "full";
|
|
@@ -247,7 +262,17 @@ export function resolveExecutionProfile({
|
|
|
247
262
|
}
|
|
248
263
|
|
|
249
264
|
const requestedRank = requested === "auto" ? PROFILE_RANK[floor] : PROFILE_RANK[requested];
|
|
250
|
-
|
|
265
|
+
let resolved = EXECUTION_PROFILES.find((profile) => PROFILE_RANK[profile] === Math.max(requestedRank, PROFILE_RANK[floor]));
|
|
266
|
+
const semanticProfile = typeof semanticRecommendation === "string"
|
|
267
|
+
? semanticRecommendation
|
|
268
|
+
: semanticRecommendation?.executionProfile?.recommendedProfile
|
|
269
|
+
?? semanticRecommendation?.recommendedProfile
|
|
270
|
+
?? null;
|
|
271
|
+
if (["light", "balanced", "full"].includes(semanticProfile)
|
|
272
|
+
&& PROFILE_RANK[semanticProfile] > PROFILE_RANK[resolved]) {
|
|
273
|
+
resolved = semanticProfile;
|
|
274
|
+
reasons.push("PROFILE_ESCALATED_BY_JEV");
|
|
275
|
+
}
|
|
251
276
|
const escalated = requested !== "auto" && PROFILE_RANK[requested] < PROFILE_RANK[floor];
|
|
252
277
|
if (escalated) reasons.push("PROFILE_ESCALATED_BY_SAFETY");
|
|
253
278
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { canonicalFingerprint } from "./artifacts.js";
|
|
2
|
+
|
|
3
|
+
export const GATE_SATISFIED_EVENT = "GATE_SATISFIED";
|
|
4
|
+
export const GATE_REVALIDATED_EVENT = "GATE_REVALIDATED";
|
|
5
|
+
const CONTRACT_REVISED_EVENT = "CONTRACT_REVISED";
|
|
6
|
+
const TRANSACTION_COMMITTED_EVENT = "TRANSACTION_COMMITTED";
|
|
7
|
+
|
|
8
|
+
function eventHash(event) {
|
|
9
|
+
const { hash, ...body } = event ?? {};
|
|
10
|
+
return canonicalFingerprint(body);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function assertGateSatisfiedDetails(details) {
|
|
14
|
+
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
15
|
+
|| Object.keys(details).length !== 1
|
|
16
|
+
|| typeof details.gate !== "string"
|
|
17
|
+
|| details.gate.trim().length === 0) {
|
|
18
|
+
const error = new Error("GATE_SATISFIED requires exactly one non-empty gate detail");
|
|
19
|
+
error.code = "E_EVENT_INVALID";
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
return details;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function assertGateRevalidatedDetails(details) {
|
|
26
|
+
const keys = ["gate", "previousGateFingerprint", "gateFingerprint", "stalePaths"];
|
|
27
|
+
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
28
|
+
|| Object.keys(details).length !== keys.length
|
|
29
|
+
|| keys.some((key) => !Object.prototype.hasOwnProperty.call(details, key))
|
|
30
|
+
|| typeof details.gate !== "string"
|
|
31
|
+
|| details.gate.trim().length === 0
|
|
32
|
+
|| !/^[a-z0-9][a-z0-9-]*$/.test(details.gate)
|
|
33
|
+
|| !/^[a-f0-9]{64}$/.test(details.previousGateFingerprint)
|
|
34
|
+
|| !/^[a-f0-9]{64}$/.test(details.gateFingerprint)
|
|
35
|
+
|| !Array.isArray(details.stalePaths)
|
|
36
|
+
|| details.stalePaths.length === 0
|
|
37
|
+
|| details.stalePaths.some((value) => typeof value !== "string" || value.trim().length === 0)
|
|
38
|
+
|| [...details.stalePaths].sort().join("\0") !== details.stalePaths.join("\0")
|
|
39
|
+
|| new Set(details.stalePaths).size !== details.stalePaths.length) {
|
|
40
|
+
const error = new Error("GATE_REVALIDATED requires a gate, two fingerprints, and sorted stale paths");
|
|
41
|
+
error.code = "E_GATE_REVALIDATION_INVALID";
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
return details;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function gateIndex(events, gateEvent, eventName) {
|
|
48
|
+
return events.findIndex((event) => event === gateEvent
|
|
49
|
+
|| (event?.seq === gateEvent?.seq
|
|
50
|
+
&& event?.taskId === gateEvent?.taskId
|
|
51
|
+
&& event?.event === eventName));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isValidGateCommit(gateEvent, commitEvent, operation) {
|
|
55
|
+
return commitEvent?.event === TRANSACTION_COMMITTED_EVENT
|
|
56
|
+
&& commitEvent.taskId === gateEvent.taskId
|
|
57
|
+
&& commitEvent.seq === gateEvent.seq + 1
|
|
58
|
+
&& commitEvent.previousHash === gateEvent.hash
|
|
59
|
+
&& gateEvent.hash === eventHash(gateEvent)
|
|
60
|
+
&& commitEvent.hash === eventHash(commitEvent)
|
|
61
|
+
&& commitEvent.details
|
|
62
|
+
&& typeof commitEvent.details === "object"
|
|
63
|
+
&& !Array.isArray(commitEvent.details)
|
|
64
|
+
&& Object.keys(commitEvent.details).length === 2
|
|
65
|
+
&& typeof commitEvent.details.transactionId === "string"
|
|
66
|
+
&& commitEvent.details.transactionId.length > 0
|
|
67
|
+
&& commitEvent.details.operation === operation;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveGateRecordBoundary(events, gateEvent) {
|
|
71
|
+
if (!Array.isArray(events) || !gateEvent || gateEvent.event !== GATE_SATISFIED_EVENT) return null;
|
|
72
|
+
const index = gateIndex(events, gateEvent, GATE_SATISFIED_EVENT);
|
|
73
|
+
if (index < 0) return null;
|
|
74
|
+
const commitEvent = events[index + 1];
|
|
75
|
+
return isValidGateCommit(gateEvent, commitEvent, "gate-record")
|
|
76
|
+
? { gateEvent, transactionCommitEvent: commitEvent }
|
|
77
|
+
: null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveGateRevalidationBoundary(events, gateEvent) {
|
|
81
|
+
if (!Array.isArray(events) || !gateEvent || gateEvent.event !== GATE_REVALIDATED_EVENT) return null;
|
|
82
|
+
const index = gateIndex(events, gateEvent, GATE_REVALIDATED_EVENT);
|
|
83
|
+
if (index < 0) return null;
|
|
84
|
+
const commitEvent = events[index + 1];
|
|
85
|
+
return isValidGateCommit(gateEvent, commitEvent, "gate-revalidate")
|
|
86
|
+
? { gateEvent, transactionCommitEvent: commitEvent }
|
|
87
|
+
: null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function hasCurrentGateEvidence(events, taskId, gate, latestContractRevisionSeq = 0) {
|
|
91
|
+
return events.some((event) => [GATE_SATISFIED_EVENT, GATE_REVALIDATED_EVENT].includes(event.event)
|
|
92
|
+
&& event.taskId === taskId
|
|
93
|
+
&& event.details?.gate === gate
|
|
94
|
+
&& event.seq > latestContractRevisionSeq);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function validateGateSatisfactionBindings(events = []) {
|
|
98
|
+
const errors = [];
|
|
99
|
+
for (const [index, event] of events.entries()) {
|
|
100
|
+
if (![GATE_SATISFIED_EVENT, GATE_REVALIDATED_EVENT].includes(event.event)) continue;
|
|
101
|
+
const revalidated = event.event === GATE_REVALIDATED_EVENT;
|
|
102
|
+
try {
|
|
103
|
+
if (revalidated) assertGateRevalidatedDetails(event.details);
|
|
104
|
+
else assertGateSatisfiedDetails(event.details);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
errors.push({ code: error.code ?? "E_EVENT_INVALID", message: `event ${event.seq} (${event.event}): ${error.message}` });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const revisedBeforeGate = events.slice(0, index).findLast((candidate) => candidate.taskId === event.taskId
|
|
110
|
+
&& candidate.event === CONTRACT_REVISED_EVENT);
|
|
111
|
+
const boundary = revalidated
|
|
112
|
+
? resolveGateRevalidationBoundary(events, event)
|
|
113
|
+
: resolveGateRecordBoundary(events, event);
|
|
114
|
+
if ((revalidated || revisedBeforeGate) && !boundary) {
|
|
115
|
+
errors.push({
|
|
116
|
+
code: revalidated ? "E_GATE_REVALIDATION_INVALID" : "E_EVENT_INVALID",
|
|
117
|
+
message: revalidated
|
|
118
|
+
? `event ${event.seq} must be immediately followed by its gate-revalidate transaction commit`
|
|
119
|
+
: `event ${event.seq} must be immediately followed by its gate-record transaction commit after contract revision`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return errors;
|
|
124
|
+
}
|