@cassiomc1/forgeloop 1.12.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/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/AGENT_COMPATIBILITY.md +8 -0
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +90 -0
- package/DOCS_INDEX.md +46 -12
- package/ENG/c-development-eng.md +112 -0
- package/ENG/cpp-development-eng.md +109 -0
- package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
- package/ENG/go-development-eng.md +103 -0
- package/ENG/java-development-eng.md +125 -0
- package/ENG/nodejs-backend-development-eng.md +605 -0
- package/ENG/php-development-eng.md +104 -0
- package/ENG/rust-development-eng.md +422 -0
- package/ENG/sec-code-eng.md +7 -7
- package/ENG/sql-development-eng.md +108 -0
- package/ENG/swift-development-eng.md +111 -0
- package/ENG/typescript-development-eng.md +108 -0
- package/EXECUTION_STATE.md +12 -0
- package/GUIDE_ROUTER.md +418 -9
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/QUALITY_SCORECARD.md +1 -0
- package/README.md +78 -52
- package/TERMINOLOGY.md +2 -0
- package/THIRD_PARTY_NOTICES.md +19 -7
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +22 -4
- package/completions/forgeloop.bash +40 -4
- package/completions/forgeloop.fish +130 -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 +81 -3
- 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 +392 -10
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +34 -12
- package/docs/GETTING_STARTED.md +59 -0
- 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 +60 -19
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +32 -0
- package/docs/RELEASE_CHECKLIST.md +66 -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 +298 -3
- 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 +1397 -0
- package/docs/protocol-requirements.json +101 -0
- package/package.json +46 -4
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/routing-input.schema.json +1 -1
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +84 -11
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +40 -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/next.js +19 -7
- 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-create.js +84 -25
- package/src/commands/task-list.js +22 -2
- 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/config/guides.json +44 -0
- 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/build-script.js +151 -0
- package/src/core/c-cpp-project.js +143 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +249 -1
- package/src/core/command-executors.js +115 -3
- package/src/core/command-input.js +212 -102
- 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-presets.js +82 -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 +281 -3
- 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/filesystem.js +1 -10
- package/src/core/gate-provenance.js +124 -0
- package/src/core/go-project.js +206 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/java-project.js +403 -0
- 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/multi-language-project.js +117 -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/next-explanation.js +63 -0
- package/src/core/phase.js +128 -10
- package/src/core/php-project.js +85 -0
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/project-detection.js +1760 -52
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +132 -53
- 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 +223 -4
- package/src/core/runtime-context.js +118 -61
- package/src/core/rust-project.js +400 -0
- 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/sql-project.js +141 -0
- package/src/core/swift-project.js +200 -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/core/typescript-project.js +349 -0
- package/src/core/xml-structure.js +123 -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
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import { canonicalFingerprint } from "./artifacts.js";
|
|
2
|
+
|
|
3
|
+
export const CONTRACT_BOOTSTRAP_REPAIR_EVENT = "CONTRACT_BOOTSTRAP_REPAIR_RECORDED";
|
|
4
|
+
export const CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_EVENT = "CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_RECORDED";
|
|
5
|
+
export const ROUTE_CHECKPOINT_BOUND_EVENT = "ROUTE_CHECKPOINT_BOUND";
|
|
6
|
+
export const CONTRACT_BOOTSTRAP_REPAIR_VERSION = 1;
|
|
7
|
+
export const CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_VERSION = 1;
|
|
8
|
+
export const CONTRACT_BOOTSTRAP_REPAIR_DEFECT = "DUPLICATE_CONTRACT_VALIDATED_AFTER_CONTRACT_READY";
|
|
9
|
+
export const CONTRACT_BOOTSTRAP_REPAIR_AUTHORITY = "CALLER_ACKNOWLEDGED";
|
|
10
|
+
export const CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_DEFECT = "CONTRACT_BOOTSTRAP_REPAIR_MARKER_MISSING_RECONSTRUCTED_STATE_REVISION";
|
|
11
|
+
|
|
12
|
+
const FINGERPRINT = /^[a-f0-9]{64}$/;
|
|
13
|
+
const REPAIR_ID = /^repair-[a-f0-9]{64}$/;
|
|
14
|
+
const HISTORICAL_EVENTS = new Set([
|
|
15
|
+
"DESIGN_GATE_STARTED",
|
|
16
|
+
"PLAN_RECORDED",
|
|
17
|
+
"EXECUTION_STARTED",
|
|
18
|
+
"VERIFICATION_STARTED",
|
|
19
|
+
"VERIFICATION_RECORDED",
|
|
20
|
+
"DIAGNOSIS_RECORDED",
|
|
21
|
+
"DIAGNOSTIC_CASE_RECORDED",
|
|
22
|
+
"CORRECTION_STARTED",
|
|
23
|
+
"REVIEW_STARTED",
|
|
24
|
+
"COMPLETION_VALIDATED",
|
|
25
|
+
"COMPLETION_REJECTED",
|
|
26
|
+
"TASK_RECOVERY_RECORDED",
|
|
27
|
+
"OPERATOR_RECOVERY_RECORDED",
|
|
28
|
+
"TASK_RECOVERY_RESUMED",
|
|
29
|
+
"TASK_ABANDONED",
|
|
30
|
+
"LEGACY_RECOVERY_MIGRATION_RECORDED",
|
|
31
|
+
]);
|
|
32
|
+
const REPAIR_DETAIL_KEYS = new Set([
|
|
33
|
+
"repairVersion", "taskId", "repairId", "defect",
|
|
34
|
+
"canonicalContractEventSeq", "canonicalContractEventHash",
|
|
35
|
+
"duplicateContractEventSeq", "duplicateContractEventHash",
|
|
36
|
+
"contractCreateCommitSeq", "contractCreateCommitHash", "contractCreateTransactionId",
|
|
37
|
+
"contractFingerprint", "reconstructedPhase", "routeFingerprint",
|
|
38
|
+
"previousStateFingerprint", "reconstructedStateFingerprint", "reconstructedStateRevision",
|
|
39
|
+
"repairedAt", "authorityKind",
|
|
40
|
+
]);
|
|
41
|
+
const LEGACY_REPAIR_DETAIL_KEYS = new Set([...REPAIR_DETAIL_KEYS].filter((key) => key !== "reconstructedStateRevision"));
|
|
42
|
+
const MIGRATION_DETAIL_KEYS = new Set([
|
|
43
|
+
"migrationVersion", "defect", "legacyMarkerSeq", "legacyMarkerHash", "legacyRepairId",
|
|
44
|
+
"legacyRepairCommitSeq", "legacyRepairCommitHash", "legacyRepairTransactionId", "taskId",
|
|
45
|
+
"contractFingerprint", "reconstructedPhase", "routeFingerprint", "reconstructedStateFingerprint",
|
|
46
|
+
"reconstructedStateRevision", "migratedAt", "authorityKind",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function invalid(message) {
|
|
50
|
+
const error = new Error(message);
|
|
51
|
+
error.code = "E_EVENT_INVALID";
|
|
52
|
+
return error;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isFingerprint(value) {
|
|
56
|
+
return typeof value === "string" && FINGERPRINT.test(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isCommitFor(event, operation) {
|
|
60
|
+
return event?.event === "TRANSACTION_COMMITTED"
|
|
61
|
+
&& event.details?.operation === operation
|
|
62
|
+
&& typeof event.details?.transactionId === "string"
|
|
63
|
+
&& event.details.transactionId.length > 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function canonicalGuideList(value) {
|
|
67
|
+
if (!Array.isArray(value) || value.some((guide) => typeof guide !== "string" || !guide)) return null;
|
|
68
|
+
const guides = [...value].sort();
|
|
69
|
+
return new Set(guides).size === guides.length ? guides : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function sameCanonicalGuideList(left, right) {
|
|
73
|
+
const a = canonicalGuideList(left);
|
|
74
|
+
const b = canonicalGuideList(right);
|
|
75
|
+
return a !== null && b !== null && a.length === b.length && a.every((guide, index) => guide === b[index]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const EXACT_DUPLICATE_CONTRACT_ERRORS = Object.freeze([
|
|
79
|
+
"lifecycle milestone must not repeat: CONTRACT_VALIDATED",
|
|
80
|
+
"CONTRACT_VALIDATED is out of lifecycle order",
|
|
81
|
+
]);
|
|
82
|
+
export const EXACT_DUPLICATE_CONTRACT_ERRORS_FROZEN = EXACT_DUPLICATE_CONTRACT_ERRORS;
|
|
83
|
+
|
|
84
|
+
export function isExactDuplicateContractChronologyError(error) {
|
|
85
|
+
return error?.code === "E_PHASE_CHRONOLOGY_INVALID"
|
|
86
|
+
&& EXACT_DUPLICATE_CONTRACT_ERRORS.includes(error.message);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function eventHash(event) {
|
|
90
|
+
const { hash, ...body } = event;
|
|
91
|
+
return canonicalFingerprint(body);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertContractBootstrapRepairDetailsInternal(details, { legacy = false } = {}) {
|
|
95
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
96
|
+
throw invalid("contract bootstrap repair requires structured details");
|
|
97
|
+
}
|
|
98
|
+
assertRepairDetailKeys(details, legacy);
|
|
99
|
+
for (const key of [
|
|
100
|
+
"taskId", "repairId", "defect", "contractCreateTransactionId", "repairedAt", "authorityKind",
|
|
101
|
+
]) {
|
|
102
|
+
if (typeof details[key] !== "string" || !details[key].trim()) {
|
|
103
|
+
throw invalid(`contract bootstrap repair details.${key} must be a non-empty string`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (details.repairVersion !== CONTRACT_BOOTSTRAP_REPAIR_VERSION) {
|
|
107
|
+
throw invalid("contract bootstrap repair details.repairVersion is unsupported");
|
|
108
|
+
}
|
|
109
|
+
if (!REPAIR_ID.test(details.repairId)) throw invalid("contract bootstrap repair details.repairId is invalid");
|
|
110
|
+
if (details.defect !== CONTRACT_BOOTSTRAP_REPAIR_DEFECT) throw invalid("contract bootstrap repair details.defect is invalid");
|
|
111
|
+
if (details.authorityKind !== CONTRACT_BOOTSTRAP_REPAIR_AUTHORITY) throw invalid("contract bootstrap repair details.authorityKind is invalid");
|
|
112
|
+
for (const key of [
|
|
113
|
+
"canonicalContractEventHash", "duplicateContractEventHash", "contractCreateCommitHash",
|
|
114
|
+
"contractFingerprint", "reconstructedStateFingerprint",
|
|
115
|
+
]) {
|
|
116
|
+
if (!isFingerprint(details[key])) throw invalid(`contract bootstrap repair details.${key} must be a lowercase SHA-256 fingerprint`);
|
|
117
|
+
}
|
|
118
|
+
for (const key of ["canonicalContractEventSeq", "duplicateContractEventSeq", "contractCreateCommitSeq"]) {
|
|
119
|
+
if (!Number.isInteger(details[key]) || details[key] < 1) throw invalid(`contract bootstrap repair details.${key} must be a positive integer`);
|
|
120
|
+
}
|
|
121
|
+
assertRepairDetailRevision(details, legacy);
|
|
122
|
+
assertRepairDetailPhase(details);
|
|
123
|
+
if (details.routeFingerprint !== null && !isFingerprint(details.routeFingerprint)) {
|
|
124
|
+
throw invalid("contract bootstrap repair details.routeFingerprint must be a fingerprint or null");
|
|
125
|
+
}
|
|
126
|
+
if (details.previousStateFingerprint !== null && !isFingerprint(details.previousStateFingerprint)) {
|
|
127
|
+
throw invalid("contract bootstrap repair details.previousStateFingerprint must be a fingerprint or null");
|
|
128
|
+
}
|
|
129
|
+
if (!Number.isFinite(Date.parse(details.repairedAt))) throw invalid("contract bootstrap repair details.repairedAt must be an ISO timestamp");
|
|
130
|
+
return details;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function assertRepairDetailKeys(details, legacy) {
|
|
134
|
+
const allowedKeys = legacy ? LEGACY_REPAIR_DETAIL_KEYS : REPAIR_DETAIL_KEYS;
|
|
135
|
+
const unexpected = Object.keys(details).find((key) => !allowedKeys.has(key));
|
|
136
|
+
if (unexpected) throw invalid(`contract bootstrap repair details contains unknown property: ${unexpected}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function assertRepairDetailRevision(details, legacy) {
|
|
140
|
+
if (!legacy && (!Number.isInteger(details.reconstructedStateRevision) || details.reconstructedStateRevision < 0)) {
|
|
141
|
+
throw invalid("contract bootstrap repair details.reconstructedStateRevision must be a non-negative integer");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function assertRepairDetailPhase(details) {
|
|
146
|
+
if (!["CONTRACT_READY", "ROUTED"].includes(details.reconstructedPhase)) {
|
|
147
|
+
throw invalid("contract bootstrap repair details.reconstructedPhase is invalid");
|
|
148
|
+
}
|
|
149
|
+
if (details.reconstructedPhase === "ROUTED" && details.routeFingerprint === null) {
|
|
150
|
+
throw invalid("ROUTED contract bootstrap repair requires routeFingerprint");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function assertContractBootstrapRepairDetails(details) {
|
|
155
|
+
return assertContractBootstrapRepairDetailsInternal(details);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function assertLegacyContractBootstrapRepairDetails(details) {
|
|
159
|
+
return assertContractBootstrapRepairDetailsInternal(details, { legacy: true });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function contractBootstrapRepairId(details) {
|
|
163
|
+
const identity = {
|
|
164
|
+
taskId: details.taskId,
|
|
165
|
+
defect: details.defect,
|
|
166
|
+
canonicalContractEventSeq: details.canonicalContractEventSeq,
|
|
167
|
+
canonicalContractEventHash: details.canonicalContractEventHash,
|
|
168
|
+
duplicateContractEventSeq: details.duplicateContractEventSeq,
|
|
169
|
+
duplicateContractEventHash: details.duplicateContractEventHash,
|
|
170
|
+
contractCreateCommitSeq: details.contractCreateCommitSeq,
|
|
171
|
+
contractCreateCommitHash: details.contractCreateCommitHash,
|
|
172
|
+
contractCreateTransactionId: details.contractCreateTransactionId,
|
|
173
|
+
contractFingerprint: details.contractFingerprint,
|
|
174
|
+
reconstructedPhase: details.reconstructedPhase,
|
|
175
|
+
routeFingerprint: details.routeFingerprint,
|
|
176
|
+
reconstructedStateRevision: details.reconstructedStateRevision,
|
|
177
|
+
};
|
|
178
|
+
return `repair-${canonicalFingerprint(identity)}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function legacyContractBootstrapRepairId(details) {
|
|
182
|
+
const identity = {
|
|
183
|
+
taskId: details.taskId,
|
|
184
|
+
defect: details.defect,
|
|
185
|
+
canonicalContractEventSeq: details.canonicalContractEventSeq,
|
|
186
|
+
canonicalContractEventHash: details.canonicalContractEventHash,
|
|
187
|
+
duplicateContractEventSeq: details.duplicateContractEventSeq,
|
|
188
|
+
duplicateContractEventHash: details.duplicateContractEventHash,
|
|
189
|
+
contractCreateCommitSeq: details.contractCreateCommitSeq,
|
|
190
|
+
contractCreateCommitHash: details.contractCreateCommitHash,
|
|
191
|
+
contractCreateTransactionId: details.contractCreateTransactionId,
|
|
192
|
+
contractFingerprint: details.contractFingerprint,
|
|
193
|
+
reconstructedPhase: details.reconstructedPhase,
|
|
194
|
+
routeFingerprint: details.routeFingerprint,
|
|
195
|
+
};
|
|
196
|
+
return `repair-${canonicalFingerprint(identity)}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isRepairTransactionCommit(event, taskId) {
|
|
200
|
+
if (!event || event.taskId !== taskId || event.event !== "TRANSACTION_COMMITTED") return false;
|
|
201
|
+
const details = event.details;
|
|
202
|
+
return details && typeof details === "object" && !Array.isArray(details)
|
|
203
|
+
&& Object.keys(details).length === 2
|
|
204
|
+
&& typeof details.transactionId === "string" && details.transactionId.length > 0
|
|
205
|
+
&& details.operation === "task-repair-contract-bootstrap"
|
|
206
|
+
&& event.hash === eventHash(event);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resolves the immutable repair marker and its transaction boundary. Events
|
|
211
|
+
* after the commit are normal lifecycle history and never become part of the
|
|
212
|
+
* repair transaction merely because they follow the marker.
|
|
213
|
+
*/
|
|
214
|
+
export function resolveContractBootstrapRepairBoundary(events, marker = null) {
|
|
215
|
+
if (!Array.isArray(events)) return null;
|
|
216
|
+
const markers = events.filter((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT);
|
|
217
|
+
if (markers.length !== 1) return null;
|
|
218
|
+
const resolvedMarker = marker ?? markers[0];
|
|
219
|
+
const markerIndex = events.indexOf(resolvedMarker);
|
|
220
|
+
if (markerIndex < 0 || resolvedMarker.seq !== markerIndex + 1) return null;
|
|
221
|
+
const repairCommit = events[markerIndex + 1];
|
|
222
|
+
if (!isRepairTransactionCommit(repairCommit, resolvedMarker.taskId)
|
|
223
|
+
|| repairCommit.seq !== resolvedMarker.seq + 1) return null;
|
|
224
|
+
return {
|
|
225
|
+
marker: resolvedMarker,
|
|
226
|
+
markerIndex,
|
|
227
|
+
repairCommit,
|
|
228
|
+
repairCommitIndex: markerIndex + 1,
|
|
229
|
+
postRepairEvents: events.slice(markerIndex + 2),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isCanonicalRouteReboundBoundary(reboundEvent, routeCommit, {
|
|
234
|
+
taskId,
|
|
235
|
+
contractFingerprint,
|
|
236
|
+
previousRouteFingerprint,
|
|
237
|
+
}) {
|
|
238
|
+
return reboundEvent?.event === "ROUTE_REBOUND"
|
|
239
|
+
&& reboundEvent.taskId === taskId
|
|
240
|
+
&& reboundEvent.hash === eventHash(reboundEvent)
|
|
241
|
+
&& reboundEvent.details?.contractFingerprint === contractFingerprint
|
|
242
|
+
&& reboundEvent.details?.previousRouteFingerprint === previousRouteFingerprint
|
|
243
|
+
&& isFingerprint(reboundEvent.details?.routeFingerprint)
|
|
244
|
+
&& reboundEvent.details.routeFingerprint !== previousRouteFingerprint
|
|
245
|
+
&& isCommitFor(routeCommit, "route")
|
|
246
|
+
&& routeCommit.taskId === taskId
|
|
247
|
+
&& routeCommit.seq === reboundEvent.seq + 1
|
|
248
|
+
&& routeCommit.previousHash === reboundEvent.hash
|
|
249
|
+
&& routeCommit.hash === eventHash(routeCommit);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Resolves an explicit, fingerprint-bound route evolution chain. A route
|
|
254
|
+
* transaction alone is never sufficient provenance: every hop must be a
|
|
255
|
+
* ROUTE_REBOUND immediately followed by its route transaction commit.
|
|
256
|
+
*/
|
|
257
|
+
export function resolveCanonicalRouteEvolution(events, {
|
|
258
|
+
taskId,
|
|
259
|
+
sourceSeq = 0,
|
|
260
|
+
sourceRouteFingerprint,
|
|
261
|
+
targetRouteFingerprint,
|
|
262
|
+
contractFingerprint,
|
|
263
|
+
} = {}) {
|
|
264
|
+
if (!Array.isArray(events)
|
|
265
|
+
|| typeof taskId !== "string" || !taskId
|
|
266
|
+
|| !Number.isInteger(sourceSeq) || sourceSeq < 0
|
|
267
|
+
|| (sourceRouteFingerprint !== null && !isFingerprint(sourceRouteFingerprint))
|
|
268
|
+
|| !isFingerprint(targetRouteFingerprint)
|
|
269
|
+
|| !isFingerprint(contractFingerprint)) return null;
|
|
270
|
+
let currentFingerprint = sourceRouteFingerprint;
|
|
271
|
+
const reboundEvents = [];
|
|
272
|
+
for (let index = 0; index < events.length - 1; index += 1) {
|
|
273
|
+
const reboundEvent = events[index];
|
|
274
|
+
if (reboundEvent?.seq <= sourceSeq || reboundEvent?.event !== "ROUTE_REBOUND") continue;
|
|
275
|
+
const routeCommit = events[index + 1];
|
|
276
|
+
if (!isCanonicalRouteReboundBoundary(reboundEvent, routeCommit, {
|
|
277
|
+
taskId,
|
|
278
|
+
contractFingerprint,
|
|
279
|
+
previousRouteFingerprint: currentFingerprint,
|
|
280
|
+
})) return null;
|
|
281
|
+
reboundEvents.push({ reboundEvent, routeCommit, previousFingerprint: currentFingerprint });
|
|
282
|
+
currentFingerprint = reboundEvent.details.routeFingerprint;
|
|
283
|
+
}
|
|
284
|
+
return currentFingerprint === targetRouteFingerprint
|
|
285
|
+
? { valid: true, reboundEvents, routeFingerprint: currentFingerprint }
|
|
286
|
+
: null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function resolveCanonicalPostRepairRouteBinding(events, marker, currentRouteFingerprint) {
|
|
290
|
+
const boundary = resolveContractBootstrapRepairBoundary(events, marker);
|
|
291
|
+
if (!boundary || !isFingerprint(currentRouteFingerprint)) return null;
|
|
292
|
+
const binding = resolveCanonicalRouteEvolution(boundary.postRepairEvents, {
|
|
293
|
+
taskId: boundary.marker.taskId,
|
|
294
|
+
sourceSeq: boundary.repairCommit.seq,
|
|
295
|
+
sourceRouteFingerprint: boundary.marker.details.routeFingerprint,
|
|
296
|
+
targetRouteFingerprint: currentRouteFingerprint,
|
|
297
|
+
contractFingerprint: boundary.marker.details.contractFingerprint,
|
|
298
|
+
});
|
|
299
|
+
if (!binding?.reboundEvents?.length) return null;
|
|
300
|
+
const last = binding.reboundEvents.at(-1);
|
|
301
|
+
return {
|
|
302
|
+
valid: true,
|
|
303
|
+
reboundEvent: last.reboundEvent,
|
|
304
|
+
routeCommit: last.routeCommit,
|
|
305
|
+
reboundEvents: binding.reboundEvents,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const CHECKPOINT_PHASE_EVENTS = new Set(["DESIGN_GATE_STARTED", "PLAN_RECORDED"]);
|
|
310
|
+
|
|
311
|
+
export function resolveCanonicalPostRepairCheckpointBinding(events, marker) {
|
|
312
|
+
const boundary = resolveContractBootstrapRepairBoundary(events, marker);
|
|
313
|
+
if (!boundary) return null;
|
|
314
|
+
const checkpointEvents = boundary.postRepairEvents.filter((event) => event.event === ROUTE_CHECKPOINT_BOUND_EVENT);
|
|
315
|
+
if (checkpointEvents.length !== 1) return null;
|
|
316
|
+
const checkpointEvent = checkpointEvents[0];
|
|
317
|
+
const checkpointIndex = boundary.postRepairEvents.indexOf(checkpointEvent);
|
|
318
|
+
const phaseEvent = boundary.postRepairEvents[checkpointIndex + 1];
|
|
319
|
+
const transactionCommit = boundary.postRepairEvents[checkpointIndex + 2];
|
|
320
|
+
const details = checkpointEvent.details;
|
|
321
|
+
if (checkpointEvent.taskId !== boundary.marker.taskId
|
|
322
|
+
|| checkpointEvent.hash !== eventHash(checkpointEvent)
|
|
323
|
+
|| details?.contractFingerprint !== boundary.marker.details.contractFingerprint
|
|
324
|
+
|| !isFingerprint(details?.routeFingerprint)
|
|
325
|
+
|| canonicalGuideList(details?.selectedGuides) === null
|
|
326
|
+
|| !CHECKPOINT_PHASE_EVENTS.has(phaseEvent?.event)
|
|
327
|
+
|| phaseEvent.taskId !== boundary.marker.taskId
|
|
328
|
+
|| phaseEvent.hash !== eventHash(phaseEvent)
|
|
329
|
+
|| !isCommitFor(transactionCommit, "advance")
|
|
330
|
+
|| transactionCommit.taskId !== boundary.marker.taskId
|
|
331
|
+
|| transactionCommit.hash !== eventHash(transactionCommit)) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
valid: true,
|
|
336
|
+
checkpointEvent,
|
|
337
|
+
phaseEvent,
|
|
338
|
+
transactionCommit,
|
|
339
|
+
routeFingerprint: details.routeFingerprint,
|
|
340
|
+
contractFingerprint: details.contractFingerprint,
|
|
341
|
+
selectedGuides: [...details.selectedGuides],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function isContractBootstrapRepairCandidate(events, ledgerErrors = [], taskId = null) {
|
|
346
|
+
if (!Array.isArray(events) || events.length < 4) return null;
|
|
347
|
+
const effectiveTaskId = taskId ?? events[0]?.taskId;
|
|
348
|
+
if (typeof effectiveTaskId !== "string" || !effectiveTaskId) return null;
|
|
349
|
+
if (events.some((event) => event.taskId !== effectiveTaskId)) return null;
|
|
350
|
+
if (events.some((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT)) return null;
|
|
351
|
+
if (events[0]?.event !== "TASK_RECEIVED" || events[1]?.event !== "TRANSACTION_COMMITTED"
|
|
352
|
+
|| events[1]?.details?.operation !== "task-create") return null;
|
|
353
|
+
if (!events.some((event) => event.event === "DISCOVERY_STARTED")) return null;
|
|
354
|
+
const contractEvents = events.filter((event) => event.event === "CONTRACT_VALIDATED");
|
|
355
|
+
if (contractEvents.length !== 2) return null;
|
|
356
|
+
const [canonical, duplicate] = contractEvents;
|
|
357
|
+
if (!isFingerprint(canonical.details?.contractFingerprint)
|
|
358
|
+
|| canonical.details.contractFingerprint !== duplicate.details?.contractFingerprint) return null;
|
|
359
|
+
if (canonical.hash !== eventHash(canonical) || duplicate.hash !== eventHash(duplicate)) return null;
|
|
360
|
+
const canonicalIndex = events.indexOf(canonical);
|
|
361
|
+
const duplicateIndex = events.indexOf(duplicate);
|
|
362
|
+
const canonicalCommit = events[canonicalIndex + 1];
|
|
363
|
+
const duplicateCommit = events[duplicateIndex + 1];
|
|
364
|
+
if (!isCommitFor(canonicalCommit, "contract-create") || !isCommitFor(duplicateCommit, "contract-create")) return null;
|
|
365
|
+
if (duplicateIndex !== events.length - 2 || events.at(-1) !== duplicateCommit) return null;
|
|
366
|
+
if (events.some((event) => HISTORICAL_EVENTS.has(event.event))) return null;
|
|
367
|
+
if (!Array.isArray(ledgerErrors) || ledgerErrors.length < 1) return null;
|
|
368
|
+
if (ledgerErrors.some((error) => !isExactDuplicateContractChronologyError(error))) return null;
|
|
369
|
+
if (!ledgerErrors.some((error) => error.message === "lifecycle milestone must not repeat: CONTRACT_VALIDATED")) return null;
|
|
370
|
+
return {
|
|
371
|
+
taskId: effectiveTaskId,
|
|
372
|
+
canonicalContractEvent: canonical,
|
|
373
|
+
duplicateContractEvent: duplicate,
|
|
374
|
+
canonicalContractCommit: canonicalCommit,
|
|
375
|
+
duplicateContractCommit: duplicateCommit,
|
|
376
|
+
contractFingerprint: canonical.details.contractFingerprint,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function isContractBootstrapRepairMarkerValid(events, marker) {
|
|
381
|
+
try {
|
|
382
|
+
if (!marker || marker.event !== CONTRACT_BOOTSTRAP_REPAIR_EVENT) return false;
|
|
383
|
+
if (!resolveContractBootstrapRepairBoundary(events, marker)) return false;
|
|
384
|
+
assertContractBootstrapRepairDetails(marker.details);
|
|
385
|
+
if (marker.taskId !== marker.details.taskId || marker.hash !== eventHash(marker)) return false;
|
|
386
|
+
if (marker.seq < 1 || events[marker.seq - 1] !== marker) return false;
|
|
387
|
+
const prefix = events.slice(0, marker.seq - 1);
|
|
388
|
+
const prefixErrors = [];
|
|
389
|
+
const candidate = isContractBootstrapRepairCandidate(prefix, prefixErrors, marker.taskId);
|
|
390
|
+
if (!candidate) {
|
|
391
|
+
const duplicateErrors = [
|
|
392
|
+
{ code: "E_PHASE_CHRONOLOGY_INVALID", message: "lifecycle milestone must not repeat: CONTRACT_VALIDATED" },
|
|
393
|
+
{ code: "E_PHASE_CHRONOLOGY_INVALID", message: "CONTRACT_VALIDATED is out of lifecycle order" },
|
|
394
|
+
];
|
|
395
|
+
const recognized = isContractBootstrapRepairCandidate(prefix, duplicateErrors, marker.taskId);
|
|
396
|
+
if (!recognized) return false;
|
|
397
|
+
return isMarkerBound(marker, recognized);
|
|
398
|
+
}
|
|
399
|
+
return isMarkerBound(marker, candidate);
|
|
400
|
+
} catch {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isMarkerBound(marker, candidate) {
|
|
406
|
+
const details = marker.details;
|
|
407
|
+
const expected = {
|
|
408
|
+
taskId: candidate.taskId,
|
|
409
|
+
defect: CONTRACT_BOOTSTRAP_REPAIR_DEFECT,
|
|
410
|
+
canonicalContractEventSeq: candidate.canonicalContractEvent.seq,
|
|
411
|
+
canonicalContractEventHash: candidate.canonicalContractEvent.hash,
|
|
412
|
+
duplicateContractEventSeq: candidate.duplicateContractEvent.seq,
|
|
413
|
+
duplicateContractEventHash: candidate.duplicateContractEvent.hash,
|
|
414
|
+
contractCreateCommitSeq: candidate.duplicateContractCommit.seq,
|
|
415
|
+
contractCreateCommitHash: candidate.duplicateContractCommit.hash,
|
|
416
|
+
contractCreateTransactionId: candidate.duplicateContractCommit.details.transactionId,
|
|
417
|
+
contractFingerprint: candidate.contractFingerprint,
|
|
418
|
+
};
|
|
419
|
+
for (const [key, value] of Object.entries(expected)) if (details[key] !== value) return false;
|
|
420
|
+
if (details.repairId !== contractBootstrapRepairId(details)) return false;
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function isLegacyContractBootstrapRepairMarkerShape(event) {
|
|
425
|
+
try {
|
|
426
|
+
if (!event || event.event !== CONTRACT_BOOTSTRAP_REPAIR_EVENT) return false;
|
|
427
|
+
assertLegacyContractBootstrapRepairDetails(event.details);
|
|
428
|
+
if (typeof event.taskId !== "string" || !event.taskId
|
|
429
|
+
|| event.taskId !== event.details.taskId
|
|
430
|
+
|| !Number.isInteger(event.seq) || event.seq < 1
|
|
431
|
+
|| !isFingerprint(event.hash)
|
|
432
|
+
|| event.hash !== eventHash(event)
|
|
433
|
+
|| event.details.repairId !== legacyContractBootstrapRepairId(event.details)) return false;
|
|
434
|
+
return true;
|
|
435
|
+
} catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function migrationInvalid(message) {
|
|
441
|
+
const error = new Error(message);
|
|
442
|
+
error.code = "E_EVENT_INVALID";
|
|
443
|
+
return error;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function assertContractBootstrapRepairMigrationDetails(details) {
|
|
447
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
448
|
+
throw migrationInvalid("contract bootstrap repair migration requires structured details");
|
|
449
|
+
}
|
|
450
|
+
const unexpected = Object.keys(details).find((key) => !MIGRATION_DETAIL_KEYS.has(key));
|
|
451
|
+
if (unexpected) throw migrationInvalid(`contract bootstrap repair migration details contains unknown property: ${unexpected}`);
|
|
452
|
+
for (const key of ["defect", "legacyRepairId", "legacyRepairTransactionId", "taskId", "migratedAt", "authorityKind"]) {
|
|
453
|
+
if (typeof details[key] !== "string" || !details[key].trim()) {
|
|
454
|
+
throw migrationInvalid(`contract bootstrap repair migration details.${key} must be a non-empty string`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (details.migrationVersion !== CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_VERSION) {
|
|
458
|
+
throw migrationInvalid("contract bootstrap repair migration details.migrationVersion is unsupported");
|
|
459
|
+
}
|
|
460
|
+
if (details.defect !== CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_DEFECT) {
|
|
461
|
+
throw migrationInvalid("contract bootstrap repair migration details.defect is invalid");
|
|
462
|
+
}
|
|
463
|
+
if (details.authorityKind !== CONTRACT_BOOTSTRAP_REPAIR_AUTHORITY) {
|
|
464
|
+
throw migrationInvalid("contract bootstrap repair migration details.authorityKind is invalid");
|
|
465
|
+
}
|
|
466
|
+
for (const key of ["legacyMarkerHash", "legacyRepairCommitHash", "contractFingerprint", "reconstructedStateFingerprint"]) {
|
|
467
|
+
if (!isFingerprint(details[key])) throw migrationInvalid(`contract bootstrap repair migration details.${key} must be a lowercase SHA-256 fingerprint`);
|
|
468
|
+
}
|
|
469
|
+
if (!/^repair-[a-f0-9]{64}$/.test(details.legacyRepairId)) {
|
|
470
|
+
throw migrationInvalid("contract bootstrap repair migration details.legacyRepairId is invalid");
|
|
471
|
+
}
|
|
472
|
+
for (const key of ["legacyMarkerSeq", "legacyRepairCommitSeq", "reconstructedStateRevision"]) {
|
|
473
|
+
if (!Number.isInteger(details[key]) || details[key] < 0) {
|
|
474
|
+
throw migrationInvalid(`contract bootstrap repair migration details.${key} must be a non-negative integer`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
assertMigrationSourceSequence(details);
|
|
478
|
+
if (!isFingerprint(details.routeFingerprint) && details.routeFingerprint !== null) {
|
|
479
|
+
throw migrationInvalid("contract bootstrap repair migration details.routeFingerprint must be a fingerprint or null");
|
|
480
|
+
}
|
|
481
|
+
if (!isFingerprint(details.reconstructedStateFingerprint)) {
|
|
482
|
+
throw migrationInvalid("contract bootstrap repair migration details.reconstructedStateFingerprint must be a fingerprint");
|
|
483
|
+
}
|
|
484
|
+
if (!Number.isFinite(Date.parse(details.migratedAt))) {
|
|
485
|
+
throw migrationInvalid("contract bootstrap repair migration details.migratedAt must be an ISO timestamp");
|
|
486
|
+
}
|
|
487
|
+
return details;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function assertMigrationSourceSequence(details) {
|
|
491
|
+
if (details.legacyMarkerSeq < 1 || details.legacyRepairCommitSeq <= details.legacyMarkerSeq) {
|
|
492
|
+
throw migrationInvalid("contract bootstrap repair migration source sequence boundary is invalid");
|
|
493
|
+
}
|
|
494
|
+
if (!["CONTRACT_READY", "ROUTED"].includes(details.reconstructedPhase)) {
|
|
495
|
+
throw migrationInvalid("contract bootstrap repair migration details.reconstructedPhase is invalid");
|
|
496
|
+
}
|
|
497
|
+
if (details.reconstructedPhase === "ROUTED" && details.routeFingerprint === null) {
|
|
498
|
+
throw migrationInvalid("ROUTED contract bootstrap repair migration requires routeFingerprint");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function migrationCommitFor(event, taskId) {
|
|
503
|
+
return event?.event === "TRANSACTION_COMMITTED"
|
|
504
|
+
&& event.taskId === taskId
|
|
505
|
+
&& event.details?.operation === "task-migrate-contract-bootstrap-repair"
|
|
506
|
+
&& typeof event.details?.transactionId === "string"
|
|
507
|
+
&& event.details.transactionId.length > 0
|
|
508
|
+
&& event.hash === eventHash(event);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function migrationMatchesLegacyRepairBoundary({ migration, marker, repairCommit }) {
|
|
512
|
+
const { details } = migration;
|
|
513
|
+
const bindings = [
|
|
514
|
+
[migration.taskId, marker.taskId],
|
|
515
|
+
[details.taskId, marker.taskId],
|
|
516
|
+
[details.legacyMarkerSeq, marker.seq],
|
|
517
|
+
[details.legacyMarkerHash, marker.hash],
|
|
518
|
+
[details.legacyRepairId, marker.details.repairId],
|
|
519
|
+
[details.legacyRepairCommitSeq, repairCommit.seq],
|
|
520
|
+
[details.legacyRepairCommitHash, repairCommit.hash],
|
|
521
|
+
[details.legacyRepairTransactionId, repairCommit.details.transactionId],
|
|
522
|
+
[details.contractFingerprint, marker.details.contractFingerprint],
|
|
523
|
+
[details.reconstructedPhase, marker.details.reconstructedPhase],
|
|
524
|
+
[details.routeFingerprint, marker.details.routeFingerprint],
|
|
525
|
+
[details.reconstructedStateFingerprint, marker.details.reconstructedStateFingerprint],
|
|
526
|
+
];
|
|
527
|
+
return bindings.every(([actual, expected]) => actual === expected);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function resolveContractBootstrapRepairMigration(events, migration) {
|
|
531
|
+
if (!Array.isArray(events) || !migration || migration.event !== CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_EVENT) return null;
|
|
532
|
+
const marker = events.find((event) => event.seq === migration.details?.legacyMarkerSeq);
|
|
533
|
+
const markerIndex = marker ? events.indexOf(marker) : -1;
|
|
534
|
+
const migrationIndex = events.indexOf(migration);
|
|
535
|
+
const commit = events[migrationIndex + 1];
|
|
536
|
+
const boundary = marker ? resolveContractBootstrapRepairBoundary(events, marker) : null;
|
|
537
|
+
if (!marker || !isLegacyContractBootstrapRepairMarkerShape(marker)
|
|
538
|
+
|| markerIndex < 0 || !boundary
|
|
539
|
+
|| migrationIndex < 0 || migration.seq !== migrationIndex + 1
|
|
540
|
+
|| migration.seq <= marker.seq
|
|
541
|
+
|| !migrationMatchesLegacyRepairBoundary({ migration, marker, repairCommit: boundary.repairCommit })
|
|
542
|
+
|| !migrationCommitFor(commit, migration.taskId)
|
|
543
|
+
|| commit.seq !== migration.seq + 1) return null;
|
|
544
|
+
return { marker, migration, migrationCommit: commit };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function validateContractBootstrapRepairMigrations(events, errors, { allowUnmigratedLegacyContractBootstrapRepairMarkers = false } = {}) {
|
|
548
|
+
const markers = events.filter((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT);
|
|
549
|
+
const migrations = events.filter((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_EVENT);
|
|
550
|
+
const migrationBySeq = new Map();
|
|
551
|
+
for (const migration of migrations) {
|
|
552
|
+
try {
|
|
553
|
+
assertContractBootstrapRepairMigrationDetails(migration.details);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
errors.push({ code: error.code ?? "E_EVENT_INVALID", message: `event ${migration.seq} (${migration.event}): ${error.message}` });
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (typeof migration.taskId !== "string" || migration.taskId !== migration.details.taskId) {
|
|
559
|
+
errors.push({
|
|
560
|
+
code: "E_EVENT_INVALID",
|
|
561
|
+
message: `event ${migration.seq} (${migration.event}): migration taskId must match details.taskId`,
|
|
562
|
+
});
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (migrationBySeq.has(migration.details.legacyMarkerSeq)) {
|
|
566
|
+
errors.push({
|
|
567
|
+
code: "E_EVENT_INVALID",
|
|
568
|
+
message: `event ${migration.seq} (${migration.event}): duplicate migration for legacy marker seq ${migration.details.legacyMarkerSeq}`,
|
|
569
|
+
});
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
migrationBySeq.set(migration.details.legacyMarkerSeq, migration);
|
|
573
|
+
}
|
|
574
|
+
if (markers.length > 1) {
|
|
575
|
+
errors.push({ code: "E_CONTRACT_BOOTSTRAP_REPAIR_INVALID", message: "contract bootstrap repair marker must occur at most once" });
|
|
576
|
+
}
|
|
577
|
+
for (const marker of markers) {
|
|
578
|
+
if (!isLegacyContractBootstrapRepairMarkerShape(marker)) continue;
|
|
579
|
+
const migration = migrationBySeq.get(marker.seq);
|
|
580
|
+
if (!migration) {
|
|
581
|
+
if (!allowUnmigratedLegacyContractBootstrapRepairMarkers) {
|
|
582
|
+
errors.push({
|
|
583
|
+
code: "E_EVENT_INVALID",
|
|
584
|
+
message: `legacy contract bootstrap repair marker ${marker.seq} is not officially migrated (run forgeloop task-migrate-contract-bootstrap-repair)`,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
migrationBySeq.delete(marker.seq);
|
|
590
|
+
const binding = resolveContractBootstrapRepairMigration(events, migration);
|
|
591
|
+
if (!binding) {
|
|
592
|
+
errors.push({
|
|
593
|
+
code: "E_LEDGER_HASH_INVALID",
|
|
594
|
+
message: `migration event ${migration.seq} does not bind legacy contract bootstrap repair marker ${marker.seq}`,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
for (const [legacySeq, migration] of migrationBySeq) {
|
|
599
|
+
errors.push({
|
|
600
|
+
code: "E_EVENT_INVALID",
|
|
601
|
+
message: `migration event ${migration.seq} references unknown legacy contract bootstrap repair marker seq ${legacySeq}`,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function isLegacyContractBootstrapRepairMigrationCandidate(events, taskId = null) {
|
|
607
|
+
if (!Array.isArray(events)) return null;
|
|
608
|
+
const markers = events.filter((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT);
|
|
609
|
+
if (markers.length !== 1) return null;
|
|
610
|
+
const marker = markers[0];
|
|
611
|
+
if (!isLegacyContractBootstrapRepairMarkerShape(marker)) return null;
|
|
612
|
+
const effectiveTaskId = taskId ?? marker.taskId;
|
|
613
|
+
if (marker.taskId !== effectiveTaskId || events.some((event) => event.taskId !== effectiveTaskId)) return null;
|
|
614
|
+
if (events.some((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_EVENT)) return null;
|
|
615
|
+
const boundary = resolveContractBootstrapRepairBoundary(events, marker);
|
|
616
|
+
if (!boundary || boundary.postRepairEvents.length > 0) return null;
|
|
617
|
+
return { taskId: effectiveTaskId, marker, repairCommit: boundary.repairCommit };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export function resolveEffectiveContractBootstrapRepairAnchor(events) {
|
|
621
|
+
if (!Array.isArray(events)) return null;
|
|
622
|
+
const markers = events.filter((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT);
|
|
623
|
+
if (markers.length !== 1) return null;
|
|
624
|
+
const sourceMarker = markers[0];
|
|
625
|
+
if (isContractBootstrapRepairMarkerValid(events, sourceMarker)) {
|
|
626
|
+
return { kind: "MODERN", sourceMarker, details: sourceMarker.details };
|
|
627
|
+
}
|
|
628
|
+
if (!isLegacyContractBootstrapRepairMarkerShape(sourceMarker)) return null;
|
|
629
|
+
const migration = events
|
|
630
|
+
.filter((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_EVENT)
|
|
631
|
+
.find((event) => event.details?.legacyMarkerSeq === sourceMarker.seq);
|
|
632
|
+
const binding = resolveContractBootstrapRepairMigration(events, migration);
|
|
633
|
+
if (!binding) return null;
|
|
634
|
+
return {
|
|
635
|
+
kind: "MIGRATED_LEGACY",
|
|
636
|
+
sourceMarker,
|
|
637
|
+
migrationEvent: migration,
|
|
638
|
+
details: {
|
|
639
|
+
...sourceMarker.details,
|
|
640
|
+
reconstructedStateRevision: migration.details.reconstructedStateRevision,
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export function repairMarkerErrors(events, errors, { allowUnmigratedLegacyContractBootstrapRepairMarkers = false } = {}) {
|
|
646
|
+
const marker = events.find((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT);
|
|
647
|
+
const boundary = resolveContractBootstrapRepairBoundary(events, marker);
|
|
648
|
+
const migratedLegacy = resolveEffectiveContractBootstrapRepairAnchor(events)?.kind === "MIGRATED_LEGACY";
|
|
649
|
+
const recognized = isContractBootstrapRepairMarkerValid(events, marker)
|
|
650
|
+
|| migratedLegacy
|
|
651
|
+
|| (allowUnmigratedLegacyContractBootstrapRepairMarkers && isLegacyContractBootstrapRepairMarkerShape(marker));
|
|
652
|
+
if (!boundary || !recognized) return errors;
|
|
653
|
+
if (boundary.postRepairEvents.some((event) => event.event === "CONTRACT_VALIDATED")) return errors;
|
|
654
|
+
return errors.filter((error) => !isExactDuplicateContractChronologyError(error));
|
|
655
|
+
}
|