@cassiomc1/forgeloop 1.13.0 → 2.0.0-rc.2
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/CONTRIBUTING.md +11 -7
- package/DOCS_INDEX.md +37 -4
- package/ENG/c-development-eng.md +2 -2
- package/ENG/cpp-development-eng.md +2 -2
- package/ENG/dotnet-aspnetcore-development-eng.md +7 -1
- package/ENG/flutter-development-eng.md +4 -2
- package/ENG/go-development-eng.md +2 -2
- package/ENG/java-development-eng.md +3 -3
- package/ENG/nodejs-backend-development-eng.md +10 -6
- package/ENG/php-development-eng.md +3 -3
- package/ENG/python-development-eng.md +205 -0
- package/ENG/rust-development-eng.md +42 -1
- package/ENG/sec-code-eng.md +7 -7
- package/ENG/sql-development-eng.md +4 -4
- package/ENG/swift-development-eng.md +5 -4
- package/ENG/test-code-eng.md +1 -1
- package/ENG/typescript-development-eng.md +4 -4
- package/EXECUTION_STATE.md +12 -0
- package/GUIDE_ROUTER.md +6 -0
- package/LICENSE +133 -21
- package/LOOP_ENGINEERING.md +28 -2
- package/LOOP_SYSTEM_DESIGN.md +10 -10
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/README.md +63 -59
- package/TERMINOLOGY.md +2 -0
- package/THIRD_PARTY_NOTICES.md +3 -4
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +29 -3
- package/completions/forgeloop.bash +55 -3
- package/completions/forgeloop.fish +183 -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 +74 -39
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +171 -35
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +512 -8
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +34 -11
- package/docs/GETTING_STARTED.md +3 -3
- 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 +46 -18
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +9 -0
- package/docs/RELEASE_CHECKLIST.md +116 -12
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SELF_HOSTED_TESTS.md +121 -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 +774 -16
- package/docs/protocol-requirements.json +24 -0
- package/package.json +43 -11
- 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/task-bundle.schema.json +7 -2
- 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 +6 -3
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/scripts/run-mcp-clean-tests.mjs +30 -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 +316 -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 +289 -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/temporary-paths.js +18 -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 +218 -174
- package/src/commands/action-show.js +5 -2
- package/src/commands/advance.js +5 -1
- package/src/commands/baseline.js +13 -11
- package/src/commands/checkpoint-revalidate.js +192 -0
- package/src/commands/complete.js +19 -3
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +266 -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 +51 -13
- package/src/commands/gate-record.js +194 -0
- package/src/commands/gate-revalidate.js +135 -0
- package/src/commands/handoff-list.js +6 -1
- package/src/commands/handoff-show.js +6 -1
- package/src/commands/migrate-protocol.js +2 -2
- package/src/commands/model-route.js +40 -0
- package/src/commands/next.js +7 -1
- package/src/commands/policy-diff.js +25 -4
- package/src/commands/progress.js +17 -11
- package/src/commands/record-diagnosis.js +3 -0
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +41 -0
- package/src/commands/status.js +6 -1
- package/src/commands/storage-backup.js +15 -0
- package/src/commands/storage-migrate.js +5 -0
- package/src/commands/storage-migration-resume.js +5 -0
- package/src/commands/storage-migration-status.js +44 -0
- package/src/commands/storage-restore-resume.js +7 -0
- package/src/commands/storage-restore.js +8 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-list.js +8 -43
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-migrate.js +3 -3
- package/src/commands/task-recover.js +10 -2
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/task-scope.js +18 -2
- package/src/commands/task-show.js +16 -2
- 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 +39 -15
- package/src/commands/validate-receipt.js +8 -1
- package/src/config/guides.json +4 -0
- package/src/core/action-ledger-projection.js +10 -3
- package/src/core/actions.js +38 -75
- package/src/core/activation.js +11 -3
- package/src/core/approvals.js +17 -51
- package/src/core/artifact-registry.js +83 -2
- package/src/core/artifacts.js +81 -27
- package/src/core/attachment-paths.js +45 -0
- package/src/core/attestation-coverage.js +6 -1
- package/src/core/attestation-verifier.js +21 -2
- package/src/core/attestation.js +17 -6
- package/src/core/audit-ux.js +514 -0
- package/src/core/audit.js +6 -1
- 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/bundles.js +160 -66
- package/src/core/checkpoint-revalidation.js +321 -0
- package/src/core/cli-command-definitions.js +350 -7
- package/src/core/code-manifest.js +9 -3
- package/src/core/command-executors.js +274 -108
- package/src/core/command-input.js +136 -44
- package/src/core/completion-artifacts.js +85 -34
- package/src/core/completion-ownership.js +19 -8
- package/src/core/completion-recovery-rebind.js +11 -1
- package/src/core/completion.js +64 -15
- package/src/core/config.js +3 -0
- package/src/core/conformance.js +2 -1
- 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/continuity-lint.js +13 -2
- package/src/core/continuity-reconciliation.js +16 -3
- package/src/core/continuity.js +16 -8
- package/src/core/contract-bootstrap-recovery.js +656 -0
- package/src/core/contract-revision.js +209 -0
- package/src/core/decision/artifact.js +70 -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 +104 -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 +138 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +181 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +35 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/diagnosis-model.js +11 -0
- package/src/core/diagnosis.js +10 -1
- package/src/core/diagnostic-projection.js +29 -12
- package/src/core/diagnostic-record.js +28 -3
- package/src/core/efficiency.js +6 -1
- package/src/core/error-codes.js +278 -0
- package/src/core/events.js +535 -147
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +36 -13
- package/src/core/execution-profile.js +63 -38
- package/src/core/execution.js +26 -40
- package/src/core/filesystem.js +11 -0
- package/src/core/gate-artifact.js +105 -19
- package/src/core/gate-provenance.js +125 -0
- package/src/core/handoff.js +26 -14
- package/src/core/history.js +39 -35
- package/src/core/hypothesis-projection.js +32 -0
- package/src/core/information-gain-projection.js +104 -100
- package/src/core/inspect.js +12 -3
- package/src/core/integration-invocation-policy.js +37 -4
- package/src/core/integration-resources.js +119 -87
- package/src/core/ledger-event-collection.js +189 -0
- package/src/core/ledger-relations.js +23 -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/native-storage.js +12 -0
- package/src/core/next-action-continuity.js +31 -9
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +131 -43
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +9 -4
- package/src/core/next-action-verification-phase.js +2 -1
- package/src/core/next-action.js +56 -9
- package/src/core/phase.js +266 -103
- package/src/core/policy-engine.js +12 -13
- package/src/core/preflight-consistency.js +89 -62
- package/src/core/preflight-loaders.js +48 -9
- package/src/core/preflight-model.js +6 -1
- package/src/core/preflight.js +29 -12
- package/src/core/progress.js +21 -10
- package/src/core/protocol-info.js +68 -0
- package/src/core/protocol-migration.js +7 -6
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +281 -67
- package/src/core/recovery-history.js +67 -47
- package/src/core/reflection.js +72 -77
- package/src/core/responsibility.js +25 -13
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +28 -4
- package/src/core/router.js +67 -1
- package/src/core/runtime-context.js +123 -61
- package/src/core/schema-validation.js +11 -3
- 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/settlement.js +12 -1
- package/src/core/signing/bundle-file.js +53 -0
- package/src/core/signing/bundle-reference.js +15 -0
- package/src/core/signing/sigstore.js +60 -33
- package/src/core/signing/statement-file.js +27 -0
- package/src/core/sources.js +37 -0
- package/src/core/storage-runtime-registry.js +41 -0
- package/src/core/structural-quality/artifacts.js +20 -14
- package/src/core/structural-quality/service.js +81 -56
- package/src/core/target-layout.js +1 -1
- package/src/core/task-claim-state.js +249 -15
- package/src/core/task-command.js +21 -1
- package/src/core/task-conflict-inspection.js +133 -102
- package/src/core/task-descriptor.js +29 -22
- package/src/core/task-discovery.js +288 -90
- package/src/core/task-lock.js +66 -306
- package/src/core/task-migration-validation.js +5 -5
- package/src/core/task-migration.js +17 -287
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/task-snapshot.js +51 -8
- package/src/core/task-storage-migration.js +18 -0
- package/src/core/templates.js +23 -8
- 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 +94 -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 +164 -105
- package/src/core/trajectory-evaluation.js +15 -3
- package/src/core/trajectory-metrics.js +47 -32
- package/src/core/transaction.js +16 -287
- package/src/core/verification-scope.js +20 -12
- package/src/core/work-state.js +100 -59
- 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/repository-index/binary-manager.js +7 -5
- package/src/storage/artifact-bytes.js +22 -0
- package/src/storage/attachment-binding.js +11 -0
- package/src/storage/attachment-files.js +82 -0
- package/src/storage/attachment-references.js +36 -0
- package/src/storage/backup.js +53 -0
- package/src/storage/capabilities.js +31 -0
- package/src/storage/connection-owner.js +119 -0
- package/src/storage/connection.js +376 -0
- package/src/storage/existing-project-scope.js +45 -0
- package/src/storage/export-destination.js +44 -0
- package/src/storage/exporter.js +284 -0
- package/src/storage/file-durability.js +46 -0
- package/src/storage/importer.js +461 -0
- package/src/storage/index.js +74 -0
- package/src/storage/leases.js +63 -0
- package/src/storage/ledger-event-snapshot.js +150 -0
- package/src/storage/ledger-relations.js +220 -0
- package/src/storage/legacy-maintenance-boundary.js +18 -0
- package/src/storage/legacy-signature.js +115 -0
- package/src/storage/maintenance-handoff.js +129 -0
- package/src/storage/maintenance-owner.js +42 -0
- package/src/storage/maintenance.js +310 -0
- package/src/storage/metadata-json.js +35 -0
- package/src/storage/migration-activate.js +59 -0
- package/src/storage/migration-archive.js +57 -0
- package/src/storage/migration-attachment-root.js +61 -0
- package/src/storage/migration-candidate.js +398 -0
- package/src/storage/migration-publication.js +143 -0
- package/src/storage/migration-rollback-admission.js +54 -0
- package/src/storage/migration-rollback-preparation.js +132 -0
- package/src/storage/migration-rollback-publication.js +183 -0
- package/src/storage/migration-rollback-release.js +66 -0
- package/src/storage/migration-rollback-source-stage.js +103 -0
- package/src/storage/migration-rollback.js +43 -0
- package/src/storage/migration-source-partition.js +56 -0
- package/src/storage/migration-source.js +255 -0
- package/src/storage/migration-terminal.js +39 -0
- package/src/storage/migration-validation.js +75 -0
- package/src/storage/migration.js +43 -0
- package/src/storage/operational-attachments.js +49 -0
- package/src/storage/operational-context.js +50 -0
- package/src/storage/project-backup.js +98 -0
- package/src/storage/project-bootstrap.js +24 -0
- package/src/storage/project-boundary.js +79 -0
- package/src/storage/project-read-snapshot.js +18 -0
- package/src/storage/project-replacement.js +137 -0
- package/src/storage/project-restore.js +203 -0
- package/src/storage/record-definitions.js +45 -0
- package/src/storage/repository.js +507 -0
- package/src/storage/restore-journal.js +62 -0
- package/src/storage/restore-layout.js +30 -0
- package/src/storage/restore-preparation.js +84 -0
- package/src/storage/restore-replacement-archive.js +162 -0
- package/src/storage/restore-replacement-owner.js +31 -0
- package/src/storage/restore-replacement-preparation.js +128 -0
- package/src/storage/restore-replacement.js +142 -0
- package/src/storage/restore-terminal.js +32 -0
- package/src/storage/runtime.js +31 -0
- package/src/storage/schema.js +214 -0
- package/src/storage/singleton-import.js +84 -0
- package/src/storage/snapshot-continuity.js +16 -0
- package/src/storage/snapshot.js +36 -0
- package/src/storage/storage-marker.js +79 -0
- package/src/storage/task-guards.js +107 -0
- package/src/storage/topology.js +42 -0
- package/src/storage/transaction.js +179 -0
- package/src/storage/unit-of-work.js +663 -0
- package/src/storage/windows-process-incarnation.js +206 -0
- package/.forgeloop/forgeloop.gitignore +0 -4
- package/LICENSE-DOCS.md +0 -13
- package/src/core/transaction-maintenance.js +0 -70
package/src/core/events.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { ledgerRelationMap, ledgerRelationSet } from "./ledger-relations.js";
|
|
2
|
+
import { ledgerEventAt, ledgerEventsOfTypes } from "./ledger-event-collection.js";
|
|
3
|
+
import { needsExistingProjectScope, withExistingProjectScope, isOperationalArtifactPath } from "../storage/existing-project-scope.js";
|
|
1
4
|
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
5
|
+
import { open, readFile, stat } from "node:fs/promises";
|
|
6
|
+
import { closeSync, lstatSync, openSync, readSync } from "node:fs";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
4
8
|
|
|
5
9
|
import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
|
|
6
10
|
import { ARTIFACT_PATHS, canonicalFingerprint } from "./artifacts.js";
|
|
@@ -9,9 +13,19 @@ import { assertSchema, readSchema } from "./schema-validation.js";
|
|
|
9
13
|
import { assertSecretFree } from "./receipt.js";
|
|
10
14
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
11
15
|
import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
|
|
16
|
+
import {
|
|
17
|
+
CONTRACT_BOOTSTRAP_REPAIR_EVENT,
|
|
18
|
+
ROUTE_CHECKPOINT_BOUND_EVENT,
|
|
19
|
+
assertContractBootstrapRepairDetails,
|
|
20
|
+
assertContractBootstrapRepairMigrationDetails,
|
|
21
|
+
isLegacyContractBootstrapRepairMarkerShape,
|
|
22
|
+
validateContractBootstrapRepairMigrations,
|
|
23
|
+
repairMarkerErrors,
|
|
24
|
+
} from "./contract-bootstrap-recovery.js";
|
|
12
25
|
|
|
13
26
|
import { taskArtifactPath } from "./task-paths.js";
|
|
14
27
|
import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
|
|
28
|
+
import { getOperationalStore } from "../storage/operational-context.js";
|
|
15
29
|
|
|
16
30
|
import { assertDiagnosisDetails } from "./diagnosis-model.js";
|
|
17
31
|
import {
|
|
@@ -33,6 +47,32 @@ import {
|
|
|
33
47
|
isLegacyRecoveryEventShape,
|
|
34
48
|
legacyRecoveryMigrationId,
|
|
35
49
|
} from "./task-recovery-migration.js";
|
|
50
|
+
import {
|
|
51
|
+
CHECKPOINT_REVALIDATED_EVENT,
|
|
52
|
+
assertCheckpointRevalidatedDetails,
|
|
53
|
+
validateCheckpointRevalidationCurrentBinding,
|
|
54
|
+
validateCheckpointRevalidationEventBindings,
|
|
55
|
+
} from "./checkpoint-revalidation.js";
|
|
56
|
+
import {
|
|
57
|
+
CONTRACT_REVISED_EVENT,
|
|
58
|
+
assertContractRevisedDetails,
|
|
59
|
+
validateContractRevisionCurrentBinding,
|
|
60
|
+
validateContractRevisionEventBindings,
|
|
61
|
+
} from "./contract-revision.js";
|
|
62
|
+
import {
|
|
63
|
+
GATE_SATISFIED_EVENT,
|
|
64
|
+
GATE_REVALIDATED_EVENT,
|
|
65
|
+
assertGateSatisfiedDetails,
|
|
66
|
+
assertGateRevalidatedDetails,
|
|
67
|
+
hasCurrentGateEvidence,
|
|
68
|
+
validateGateSatisfactionBindings,
|
|
69
|
+
} from "./gate-provenance.js";
|
|
70
|
+
import {
|
|
71
|
+
assertSemanticDecisionDetails,
|
|
72
|
+
validateSemanticDecisionArtifactBindings,
|
|
73
|
+
validateSemanticDecisionEventBindings,
|
|
74
|
+
} from "./decision/events.js";
|
|
75
|
+
import { SEMANTIC_DECISION_RECORDED_EVENT, SEMANTIC_DECISION_SUPERSEDED_EVENT } from "./decision/constants.js";
|
|
36
76
|
|
|
37
77
|
const EVENT_SCHEMA_VERSION = 1;
|
|
38
78
|
export const LIFECYCLE_MILESTONES = Object.freeze([
|
|
@@ -60,19 +100,45 @@ const REPEATABLE_MILESTONES = new Set([
|
|
|
60
100
|
"PREFLIGHT_READY",
|
|
61
101
|
]);
|
|
62
102
|
|
|
63
|
-
function
|
|
64
|
-
return
|
|
103
|
+
export function isRevisionEpochPlanRepeat(events = [], index = 0, event = null) {
|
|
104
|
+
if (event?.event !== "PLAN_RECORDED") return false;
|
|
105
|
+
const previousPlan = events.slice(0, index).findLast((candidate) => candidate.event === "PLAN_RECORDED");
|
|
106
|
+
const revision = events.slice(0, index).findLast((candidate) => candidate.event === CONTRACT_REVISED_EVENT);
|
|
107
|
+
return Boolean(previousPlan && revision && previousPlan.seq < revision.seq);
|
|
65
108
|
}
|
|
66
109
|
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
110
|
+
function validateMilestoneChronology(events, index, event, milestoneCounts, lastMilestone, errors) {
|
|
111
|
+
const milestoneIndex = LIFECYCLE_MILESTONES.indexOf(event.event);
|
|
112
|
+
if (milestoneIndex < 0) return lastMilestone;
|
|
113
|
+
const count = (milestoneCounts.get(event.event) ?? 0) + 1;
|
|
114
|
+
milestoneCounts.set(event.event, count);
|
|
115
|
+
const revisionPlanRepeat = isRevisionEpochPlanRepeat(events, index, event);
|
|
116
|
+
if (count > 1 && !REPEATABLE_MILESTONES.has(event.event) && !revisionPlanRepeat) {
|
|
117
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
|
|
118
|
+
}
|
|
119
|
+
if (milestoneIndex > lastMilestone + 1) {
|
|
120
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `${event.event} is missing prerequisite milestone: ${LIFECYCLE_MILESTONES[lastMilestone + 1]}` });
|
|
121
|
+
} else if (milestoneIndex < lastMilestone && event.event !== "VERIFICATION_STARTED" && event.event !== "PREFLIGHT_READY") {
|
|
122
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `${event.event} is out of lifecycle order` });
|
|
123
|
+
} else if (milestoneIndex === lastMilestone && !REPEATABLE_MILESTONES.has(event.event) && !revisionPlanRepeat) {
|
|
124
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
|
|
125
|
+
}
|
|
126
|
+
if (milestoneIndex > lastMilestone) return milestoneIndex;
|
|
127
|
+
return lastMilestone;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validateExecutionGateChronology(events, index, seen, errors) {
|
|
131
|
+
if (!seen.has("ROUTE_VALIDATED")) errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before route validation" });
|
|
132
|
+
if (!seen.has("CONTRACT_VALIDATED")) errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before contract validation" });
|
|
133
|
+
if (!seen.has("PREFLIGHT_READY")) errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before preflight readiness" });
|
|
134
|
+
const preflight = events.slice(0, index).findLast((candidate) => candidate.event === "PREFLIGHT_READY");
|
|
135
|
+
const requiredGates = preflight?.details?.requiredGates ?? [];
|
|
136
|
+
const latestContractRevisionSeq = events.slice(0, index).findLast((candidate) => candidate.event === CONTRACT_REVISED_EVENT)?.seq ?? 0;
|
|
137
|
+
const beforeExecution = events.slice(0, index);
|
|
138
|
+
for (const gate of requiredGates) {
|
|
139
|
+
if (!hasCurrentGateEvidence(beforeExecution, ledgerEventAt(events, index)?.taskId, gate, latestContractRevisionSeq)) {
|
|
140
|
+
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `execution started before gate satisfaction: ${gate}` });
|
|
141
|
+
}
|
|
76
142
|
}
|
|
77
143
|
}
|
|
78
144
|
|
|
@@ -89,17 +155,7 @@ async function readEventCheckpoint(target, packageRoot, relPath, options, transa
|
|
|
89
155
|
if (!transaction.eventCheckpoints) transaction.eventCheckpoints = new Map();
|
|
90
156
|
const cached = transaction.eventCheckpoints.get(relPath);
|
|
91
157
|
if (cached) return cached;
|
|
92
|
-
|
|
93
|
-
const indexPath = eventIndexPath(relPath);
|
|
94
|
-
const indexed = parseEventIndex(await transaction.readText(indexPath), indexPath);
|
|
95
|
-
const tail = await readEventTail(target, packageRoot, { ...options, eventsPath: relPath, limit: 1 });
|
|
96
|
-
const last = tail.at(-1) ?? null;
|
|
97
|
-
let checkpoint;
|
|
98
|
-
if (indexed && indexed.seq === (last?.seq ?? 0) && indexed.lastHash === (last?.hash ?? null)) {
|
|
99
|
-
checkpoint = indexed;
|
|
100
|
-
} else {
|
|
101
|
-
checkpoint = checkpointFromEvents(await readEvents(target, packageRoot, { ...options, eventsPath: relPath }));
|
|
102
|
-
}
|
|
158
|
+
const checkpoint = checkpointFromEvents(await readEventTail(target, packageRoot, { ...options, eventsPath: relPath, limit: 1 }));
|
|
103
159
|
transaction.eventCheckpoints.set(relPath, checkpoint);
|
|
104
160
|
return checkpoint;
|
|
105
161
|
}
|
|
@@ -125,6 +181,22 @@ export function validateKnownEventDetails(event) {
|
|
|
125
181
|
case "CHECKPOINT_RECONCILED":
|
|
126
182
|
assertReconcileClosureDetails(event.details);
|
|
127
183
|
return;
|
|
184
|
+
case CHECKPOINT_REVALIDATED_EVENT:
|
|
185
|
+
assertCheckpointRevalidatedDetails(event.details);
|
|
186
|
+
return;
|
|
187
|
+
case CONTRACT_REVISED_EVENT:
|
|
188
|
+
assertContractRevisedDetails(event.details);
|
|
189
|
+
return;
|
|
190
|
+
case GATE_SATISFIED_EVENT:
|
|
191
|
+
assertGateSatisfiedDetails(event.details);
|
|
192
|
+
return;
|
|
193
|
+
case GATE_REVALIDATED_EVENT:
|
|
194
|
+
assertGateRevalidatedDetails(event.details);
|
|
195
|
+
return;
|
|
196
|
+
case SEMANTIC_DECISION_RECORDED_EVENT:
|
|
197
|
+
case SEMANTIC_DECISION_SUPERSEDED_EVENT:
|
|
198
|
+
assertSemanticDecisionDetails(event.details, event.event);
|
|
199
|
+
return;
|
|
128
200
|
case "TASK_RECOVERY_RECORDED":
|
|
129
201
|
assertRecoveryRecordedDetails(event.details);
|
|
130
202
|
return;
|
|
@@ -135,18 +207,27 @@ export function validateKnownEventDetails(event) {
|
|
|
135
207
|
if (!event.details?.recoveryId && isLegacyRecoveryDetailsShape(event.details)) return;
|
|
136
208
|
assertRecoveryRecordedDetails(event.details);
|
|
137
209
|
return;
|
|
210
|
+
case "TASK_ABANDONED":
|
|
211
|
+
assertTaskAbandonedDetails(event.details);
|
|
212
|
+
return;
|
|
138
213
|
case "LEGACY_RECOVERY_MIGRATION_RECORDED":
|
|
139
214
|
assertLegacyMigrationDetails(event.details);
|
|
140
215
|
return;
|
|
216
|
+
case CONTRACT_BOOTSTRAP_REPAIR_EVENT:
|
|
217
|
+
case "CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_RECORDED":
|
|
218
|
+
validateKnownContractBootstrapEvent(event);
|
|
219
|
+
return;
|
|
220
|
+
case "ROUTE_REBOUND":
|
|
221
|
+
assertRouteReboundDetails(event.details);
|
|
222
|
+
return;
|
|
223
|
+
case ROUTE_CHECKPOINT_BOUND_EVENT:
|
|
224
|
+
assertRouteCheckpointBoundDetails(event.details);
|
|
225
|
+
return;
|
|
141
226
|
case "TASK_RECOVERY_RESUMED":
|
|
142
227
|
assertRecoveryResumedDetails(event.details);
|
|
143
228
|
return;
|
|
144
229
|
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
|
-
}
|
|
230
|
+
assertTrajectoryEvaluatedDetails(event);
|
|
150
231
|
return;
|
|
151
232
|
case "WORKSPACE_BOUND":
|
|
152
233
|
assertStructuredArtifactEvent(event, ["workspaceFingerprint"], "WORKSPACE_BOUND");
|
|
@@ -161,14 +242,7 @@ export function validateKnownEventDetails(event) {
|
|
|
161
242
|
assertFingerprint(event.details.digest, "HANDOFF_CREATED details.digest");
|
|
162
243
|
return;
|
|
163
244
|
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");
|
|
245
|
+
assertHandoffAcceptedDetails(event);
|
|
172
246
|
return;
|
|
173
247
|
case "RESPONSIBILITY_SET":
|
|
174
248
|
assertStructuredArtifactEvent(event, ["responsibilityFingerprint"], "RESPONSIBILITY_SET");
|
|
@@ -207,6 +281,34 @@ export function validateKnownEventDetails(event) {
|
|
|
207
281
|
}
|
|
208
282
|
}
|
|
209
283
|
|
|
284
|
+
function validateKnownContractBootstrapEvent(event) {
|
|
285
|
+
if (event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT) {
|
|
286
|
+
if (isLegacyContractBootstrapRepairMarkerShape(event)) return;
|
|
287
|
+
assertContractBootstrapRepairDetails(event.details);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
assertContractBootstrapRepairMigrationDetails(event.details);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function assertTrajectoryEvaluatedDetails(event) {
|
|
294
|
+
if (!event.details || !/^eval-[A-Za-z0-9_-]+$/.test(event.details.evaluationId ?? "")
|
|
295
|
+
|| typeof event.details.scenarioId !== "string" || !/^[a-f0-9]{64}$/.test(event.details.evaluationFingerprint ?? "")
|
|
296
|
+
|| event.fingerprint !== event.details.evaluationFingerprint) {
|
|
297
|
+
throw protocolError("E_EVENT_INVALID", "TRAJECTORY_EVALUATED requires a bound evaluationId, scenarioId, and fingerprint");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function assertHandoffAcceptedDetails(event) {
|
|
302
|
+
if (!event.details || typeof event.details !== "object" || Array.isArray(event.details)
|
|
303
|
+
|| typeof event.details.handoffId !== "string" || !/^handoff-[A-Za-z0-9_-]+$/.test(event.details.handoffId)
|
|
304
|
+
|| typeof event.details.handoffDigest !== "string"
|
|
305
|
+
|| typeof event.details.consumerId !== "string" || !event.details.consumerId.trim()
|
|
306
|
+
|| (event.details.harness !== undefined && (typeof event.details.harness !== "string" || !event.details.harness.trim()))) {
|
|
307
|
+
throw protocolError("E_EVENT_INVALID", "HANDOFF_ACCEPTED requires a valid handoffId, handoffDigest, and consumerId");
|
|
308
|
+
}
|
|
309
|
+
assertFingerprint(event.details.handoffDigest, "HANDOFF_ACCEPTED details.handoffDigest");
|
|
310
|
+
}
|
|
311
|
+
|
|
210
312
|
function assertStringList(value, label) {
|
|
211
313
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item)) {
|
|
212
314
|
throw protocolError("E_EVENT_INVALID", `${label} must be an array of non-empty strings`);
|
|
@@ -234,6 +336,35 @@ function assertStructuredArtifactEvent(event, requiredDetails, label) {
|
|
|
234
336
|
}
|
|
235
337
|
}
|
|
236
338
|
|
|
339
|
+
function assertRouteReboundDetails(details) {
|
|
340
|
+
const keys = ["routeFingerprint", "contractFingerprint", "previousRouteFingerprint"];
|
|
341
|
+
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
342
|
+
|| Object.keys(details).length !== keys.length
|
|
343
|
+
|| keys.some((key) => !Object.prototype.hasOwnProperty.call(details, key))) {
|
|
344
|
+
throw protocolError("E_EVENT_INVALID", "ROUTE_REBOUND requires exactly route, contract, and previous route fingerprints");
|
|
345
|
+
}
|
|
346
|
+
assertFingerprint(details.routeFingerprint, "ROUTE_REBOUND details.routeFingerprint");
|
|
347
|
+
assertFingerprint(details.contractFingerprint, "ROUTE_REBOUND details.contractFingerprint");
|
|
348
|
+
if (details.previousRouteFingerprint !== null) {
|
|
349
|
+
assertFingerprint(details.previousRouteFingerprint, "ROUTE_REBOUND details.previousRouteFingerprint");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function assertRouteCheckpointBoundDetails(details) {
|
|
354
|
+
const keys = ["routeFingerprint", "contractFingerprint", "selectedGuides"];
|
|
355
|
+
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
356
|
+
|| Object.keys(details).length !== keys.length
|
|
357
|
+
|| keys.some((key) => !Object.prototype.hasOwnProperty.call(details, key))) {
|
|
358
|
+
throw protocolError("E_EVENT_INVALID", "ROUTE_CHECKPOINT_BOUND requires exactly route, contract, and selected guide identity");
|
|
359
|
+
}
|
|
360
|
+
assertFingerprint(details.routeFingerprint, "ROUTE_CHECKPOINT_BOUND details.routeFingerprint");
|
|
361
|
+
assertFingerprint(details.contractFingerprint, "ROUTE_CHECKPOINT_BOUND details.contractFingerprint");
|
|
362
|
+
assertStringList(details.selectedGuides, "ROUTE_CHECKPOINT_BOUND details.selectedGuides");
|
|
363
|
+
if (new Set(details.selectedGuides).size !== details.selectedGuides.length) {
|
|
364
|
+
throw protocolError("E_EVENT_INVALID", "ROUTE_CHECKPOINT_BOUND details.selectedGuides must not contain duplicates");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
237
368
|
function assertRecoveryRecordedDetails(details) {
|
|
238
369
|
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
239
370
|
throw protocolError("E_EVENT_INVALID", "recovery event requires structured details");
|
|
@@ -256,6 +387,44 @@ function assertRecoveryRecordedDetails(details) {
|
|
|
256
387
|
assertStringList(details.releasedClaims, "recovery event details.releasedClaims");
|
|
257
388
|
}
|
|
258
389
|
|
|
390
|
+
function assertTaskAbandonedDetails(details) {
|
|
391
|
+
const keys = [
|
|
392
|
+
"recoveryId", "classification", "reasonCodes", "previousPhase", "previousRevision",
|
|
393
|
+
"previousHead", "previousBranch", "currentHead", "currentBranch",
|
|
394
|
+
"releasedClaims", "authorityKind",
|
|
395
|
+
];
|
|
396
|
+
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
397
|
+
|| Object.keys(details).length !== keys.length
|
|
398
|
+
|| keys.some((key) => !Object.prototype.hasOwnProperty.call(details, key))) {
|
|
399
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED requires the exact abandonment boundary details");
|
|
400
|
+
}
|
|
401
|
+
if (typeof details.recoveryId !== "string" || details.recoveryId.length === 0) {
|
|
402
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.recoveryId must be a non-empty string");
|
|
403
|
+
}
|
|
404
|
+
if (details.classification !== "ABANDONED") {
|
|
405
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.classification must be ABANDONED");
|
|
406
|
+
}
|
|
407
|
+
assertStringList(details.reasonCodes, "TASK_ABANDONED details.reasonCodes");
|
|
408
|
+
if (details.reasonCodes.length !== 1 || details.reasonCodes[0] !== "CALLER_ABANDONED") {
|
|
409
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.reasonCodes must be [CALLER_ABANDONED]");
|
|
410
|
+
}
|
|
411
|
+
if (typeof details.previousPhase !== "string" || details.previousPhase.length === 0) {
|
|
412
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.previousPhase must be a non-empty string");
|
|
413
|
+
}
|
|
414
|
+
if (!Number.isInteger(details.previousRevision) || details.previousRevision < 0) {
|
|
415
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.previousRevision must be a non-negative integer");
|
|
416
|
+
}
|
|
417
|
+
for (const key of ["previousHead", "previousBranch", "currentHead", "currentBranch"]) {
|
|
418
|
+
if (typeof details[key] !== "string" && details[key] !== null) {
|
|
419
|
+
throw protocolError("E_EVENT_INVALID", `TASK_ABANDONED details.${key} must be a string or null`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
assertStringList(details.releasedClaims, "TASK_ABANDONED details.releasedClaims");
|
|
423
|
+
if (details.authorityKind !== "CALLER_ACKNOWLEDGED") {
|
|
424
|
+
throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.authorityKind must be CALLER_ACKNOWLEDGED");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
259
428
|
function assertRecoveryResumedDetails(details) {
|
|
260
429
|
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
261
430
|
|| typeof details.recoveryId !== "string" || !details.recoveryId) {
|
|
@@ -334,35 +503,79 @@ function protocolError(code, message, artifacts = [ARTIFACT_PATHS.events]) {
|
|
|
334
503
|
}
|
|
335
504
|
|
|
336
505
|
export async function readEvents(target, packageRoot, options = {}) {
|
|
506
|
+
if (await needsExistingProjectScope(target)) {
|
|
507
|
+
return withExistingProjectScope(target, () => readEvents(target, packageRoot, options), { readOnly: true });
|
|
508
|
+
}
|
|
337
509
|
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
510
|
+
const store = getOperationalStore(target);
|
|
511
|
+
if (store?.recognizes(relPath)) {
|
|
512
|
+
const schema = await readSchema("event", packageRoot);
|
|
513
|
+
const events = [];
|
|
514
|
+
// Array callers consume the synchronous cursor directly. Streaming callers
|
|
515
|
+
// retain iterateEvents; both paths validate and fully bind observed rows.
|
|
516
|
+
for (const event of store.iterateEvents(relPath)) {
|
|
517
|
+
validateStoredEvent(event, schema, `${relPath}[${events.length}]`);
|
|
518
|
+
events.push(event);
|
|
519
|
+
}
|
|
520
|
+
return events;
|
|
344
521
|
}
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
522
|
+
if (await assertOperationalLedgerAbsent(target, relPath)) return [];
|
|
523
|
+
return readPortableEvents(target, packageRoot, { ...options, eventsPath: relPath });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function assertOperationalLedgerAbsent(target, relativePath) {
|
|
527
|
+
await assertSafePath(target, relativePath);
|
|
528
|
+
if (isOperationalArtifactPath(relativePath) && await fileExists(ensureWithin(target, relativePath))) {
|
|
529
|
+
throw protocolError("E_STORAGE_MIGRATION_REQUIRED", "Operational ledger reads require canonical SQLite; inspect legacy exports explicitly", [relativePath]);
|
|
530
|
+
}
|
|
531
|
+
return isOperationalArtifactPath(relativePath);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Explicit read-only inspection of an import/export ledger; never selects native authority. */
|
|
535
|
+
export async function readPortableEvents(target, packageRoot, options = {}) {
|
|
536
|
+
const relPath = options.eventsPath ?? options.relativePath ?? (options.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
537
|
+
const filename = await assertSafePath(target, relPath);
|
|
538
|
+
if (!(await fileExists(filename))) return [];
|
|
539
|
+
return parseEventsText(await readFile(filename, "utf8"), relPath, packageRoot);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Stream schema-validated canonical rows within the captured operational scope. */
|
|
543
|
+
export function iterateEvents(target, packageRoot, options = {}) {
|
|
544
|
+
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
545
|
+
const store = getOperationalStore(target);
|
|
546
|
+
return (async function* validatedEvents() {
|
|
547
|
+
if (!store?.recognizes(relPath)) { yield* await readEvents(target, packageRoot, options); return; }
|
|
548
|
+
const schema = await readSchema("event", packageRoot);
|
|
549
|
+
let index = 0;
|
|
550
|
+
for (const event of store.iterateEvents(relPath)) {
|
|
551
|
+
validateStoredEvent(event, schema, `${relPath}[${index}]`);
|
|
552
|
+
index += 1;
|
|
553
|
+
yield event;
|
|
554
|
+
}
|
|
555
|
+
})();
|
|
348
556
|
}
|
|
349
557
|
|
|
350
558
|
export async function readEventTail(target, packageRoot, options = {}) {
|
|
559
|
+
if (await needsExistingProjectScope(target)) {
|
|
560
|
+
return withExistingProjectScope(target, () => readEventTail(target, packageRoot, options), { readOnly: true });
|
|
561
|
+
}
|
|
351
562
|
const limit = options.limit ?? 50;
|
|
352
563
|
if (!Number.isInteger(limit) || limit < 1) {
|
|
353
564
|
throw protocolError("E_EVENT_INVALID", "ledger tail limit must be a positive integer");
|
|
354
565
|
}
|
|
355
566
|
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
567
|
+
const store = getOperationalStore(target);
|
|
568
|
+
if (store?.recognizes(relPath)) return validateStoredEvents(store.readEvents(relPath, limit), relPath, packageRoot);
|
|
569
|
+
if (await assertOperationalLedgerAbsent(target, relPath)) return [];
|
|
570
|
+
return readPortableEventTail(target, packageRoot, { ...options, eventsPath: relPath });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Bounded tail inspection for explicit legacy/import/export tooling. */
|
|
574
|
+
export async function readPortableEventTail(target, packageRoot, options = {}) {
|
|
575
|
+
const limit = options.limit ?? 50;
|
|
576
|
+
if (!Number.isInteger(limit) || limit < 1) throw protocolError("E_EVENT_INVALID", "ledger tail limit must be a positive integer");
|
|
577
|
+
const relPath = options.eventsPath ?? options.relativePath ?? (options.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
578
|
+
const eventsPath = await assertSafePath(target, relPath);
|
|
366
579
|
if (!(await fileExists(eventsPath))) return [];
|
|
367
580
|
const size = (await stat(eventsPath)).size;
|
|
368
581
|
let window = Math.min(size, 64 * 1024);
|
|
@@ -410,18 +623,41 @@ async function parseEventsText(text, relPath, packageRoot) {
|
|
|
410
623
|
});
|
|
411
624
|
}
|
|
412
625
|
|
|
626
|
+
async function validateStoredEvents(events, relativePath, packageRoot) {
|
|
627
|
+
const schema = await readSchema("event", packageRoot);
|
|
628
|
+
for (const [index, event] of events.entries()) {
|
|
629
|
+
validateStoredEvent(event, schema, `${relativePath}[${index}]`);
|
|
630
|
+
}
|
|
631
|
+
return events;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function validateStoredEvent(event, schema, label) {
|
|
635
|
+
assertJsonBytes(JSON.stringify(event), label);
|
|
636
|
+
assertJsonLimits(event, label);
|
|
637
|
+
assertSchema(event, schema, label);
|
|
638
|
+
validateKnownEventDetails(event);
|
|
639
|
+
}
|
|
640
|
+
|
|
413
641
|
export async function appendProtocolEvent(target, input, packageRoot, options = {}) {
|
|
414
642
|
const activeTransaction = (await getTaskTransaction(target));
|
|
415
643
|
if (typeof input?.taskId !== "string" || !input.taskId) throw protocolError("E_EVENT_INVALID", "event taskId is required");
|
|
416
644
|
if (typeof input?.event !== "string" || !input.event) throw protocolError("E_EVENT_INVALID", "event type is required");
|
|
417
645
|
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
418
646
|
if (!activeTransaction) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
647
|
+
// Only a standalone append is replayable: nested domain mutations and
|
|
648
|
+
// external observations never enter this retry boundary.
|
|
649
|
+
const capturedInput = structuredClone(input);
|
|
650
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
651
|
+
try {
|
|
652
|
+
return await withTaskTransaction({
|
|
653
|
+
target,
|
|
654
|
+
taskId: options.taskId ?? capturedInput.taskId,
|
|
655
|
+
operation: "append-event",
|
|
656
|
+
}, () => appendProtocolEvent(target, capturedInput, packageRoot, options));
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (error.code !== "E_STATE_REVISION_CONFLICT" || attempt >= 2) throw error;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
425
661
|
}
|
|
426
662
|
const checkpoint = await readEventCheckpoint(target, packageRoot, relPath, options, activeTransaction);
|
|
427
663
|
const event = buildProtocolEvent(input, { checkpoint });
|
|
@@ -429,16 +665,8 @@ export async function appendProtocolEvent(target, input, packageRoot, options =
|
|
|
429
665
|
assertSchema(event, schema, relPath);
|
|
430
666
|
event.hash = eventHash(event);
|
|
431
667
|
if (!options.dryRun) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const nextCheckpoint = { schemaVersion: 1, seq: event.seq, lastHash: event.hash };
|
|
435
|
-
await activeTransaction.stageText(eventIndexPath(relPath), `${JSON.stringify(nextCheckpoint)}\n`);
|
|
436
|
-
activeTransaction.eventCheckpoints.set(relPath, nextCheckpoint);
|
|
437
|
-
} else {
|
|
438
|
-
const eventsPath = ensureWithin(target, relPath);
|
|
439
|
-
await mkdir(path.dirname(eventsPath), { recursive: true });
|
|
440
|
-
await appendFile(eventsPath, `${JSON.stringify(event)}\n`, { encoding: "utf8" });
|
|
441
|
-
}
|
|
668
|
+
activeTransaction.appendEvent(event);
|
|
669
|
+
activeTransaction.eventCheckpoints.set(relPath, { schemaVersion: 1, seq: event.seq, lastHash: event.hash });
|
|
442
670
|
}
|
|
443
671
|
return event;
|
|
444
672
|
}
|
|
@@ -450,8 +678,8 @@ export async function appendProtocolEvent(target, input, packageRoot, options =
|
|
|
450
678
|
* before appending the migration events.
|
|
451
679
|
*/
|
|
452
680
|
function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegacyRecoveryEvents = false } = {}) {
|
|
453
|
-
const migrationBySeq =
|
|
454
|
-
for (const event of events) {
|
|
681
|
+
const migrationBySeq = ledgerRelationMap();
|
|
682
|
+
for (const event of ledgerEventsOfTypes(events, [LEGACY_RECOVERY_MIGRATION_EVENT])) {
|
|
455
683
|
if (event.event !== LEGACY_RECOVERY_MIGRATION_EVENT) continue;
|
|
456
684
|
try {
|
|
457
685
|
assertLegacyMigrationDetails(event.details);
|
|
@@ -466,11 +694,12 @@ function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegac
|
|
|
466
694
|
});
|
|
467
695
|
continue;
|
|
468
696
|
}
|
|
469
|
-
migrationBySeq.set(event.details.legacyEventSeq, event);
|
|
697
|
+
migrationBySeq.set(event.details.legacyEventSeq, events.indexOf(event));
|
|
470
698
|
}
|
|
471
|
-
for (const event of events) {
|
|
699
|
+
for (const event of ledgerEventsOfTypes(events, ["OPERATOR_RECOVERY_RECORDED"])) {
|
|
472
700
|
if (!isLegacyRecoveryEventShape(event)) continue;
|
|
473
|
-
const
|
|
701
|
+
const migrationPosition = migrationBySeq.get(event.seq);
|
|
702
|
+
const migration = migrationPosition === undefined ? undefined : ledgerEventAt(events, migrationPosition);
|
|
474
703
|
if (!migration) {
|
|
475
704
|
if (!allowUnmigratedLegacyRecoveryEvents) {
|
|
476
705
|
errors.push({
|
|
@@ -502,7 +731,8 @@ function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegac
|
|
|
502
731
|
});
|
|
503
732
|
}
|
|
504
733
|
}
|
|
505
|
-
for (const [legacySeq,
|
|
734
|
+
for (const [legacySeq, position] of migrationBySeq) {
|
|
735
|
+
const migration = ledgerEventAt(events, position);
|
|
506
736
|
errors.push({
|
|
507
737
|
code: "E_EVENT_INVALID",
|
|
508
738
|
message: `migration event ${migration.seq} references unknown legacy recovery event seq ${legacySeq}`,
|
|
@@ -510,30 +740,37 @@ function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegac
|
|
|
510
740
|
}
|
|
511
741
|
}
|
|
512
742
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
743
|
+
/**
|
|
744
|
+
* Pure ledger validation over an already-loaded event array.
|
|
745
|
+
*
|
|
746
|
+
* This is the smallest reusable seam between the filesystem path and a
|
|
747
|
+
* database-backed reader: every rule below is computed from the events alone,
|
|
748
|
+
* with no filesystem access. `validateEventLedger` keeps its existing signature
|
|
749
|
+
* and delegates here after reading events, so behavior is unchanged for every
|
|
750
|
+
* existing caller.
|
|
751
|
+
*
|
|
752
|
+
* Callers that already hold events (for example a database reader) can obtain
|
|
753
|
+
* exactly the same guarantees without materializing files first.
|
|
754
|
+
*/
|
|
755
|
+
export function validateLedgerEvents(events, options = {}) {
|
|
521
756
|
const errors = [];
|
|
522
757
|
let taskId = null;
|
|
758
|
+
let previousHash = null;
|
|
523
759
|
const seen = new Set();
|
|
524
760
|
let lastMilestone = -1;
|
|
525
761
|
const milestoneCounts = new Map();
|
|
526
|
-
const createdHandoffs =
|
|
527
|
-
const acceptedHandoffs =
|
|
762
|
+
const createdHandoffs = ledgerRelationMap();
|
|
763
|
+
const acceptedHandoffs = ledgerRelationSet();
|
|
528
764
|
for (const [index, event] of events.entries()) {
|
|
529
765
|
if (event.seq !== index + 1) {
|
|
530
766
|
errors.push({ code: "E_EVENT_INVALID", message: `event sequence must be ${index + 1}` });
|
|
531
767
|
}
|
|
532
768
|
if (taskId === null) taskId = event.taskId;
|
|
533
769
|
if (event.taskId !== taskId) errors.push({ code: "E_EVENT_INVALID", message: "event task IDs must remain stable" });
|
|
534
|
-
if (event.previousHash !==
|
|
770
|
+
if (event.previousHash !== previousHash) {
|
|
535
771
|
errors.push({ code: "E_LEDGER_HASH_INVALID", message: `event ${event.seq} previousHash does not match` });
|
|
536
772
|
}
|
|
773
|
+
previousHash = event.hash;
|
|
537
774
|
if (event.hash !== eventHash(event)) {
|
|
538
775
|
errors.push({ code: "E_LEDGER_HASH_INVALID", message: `event ${event.seq} hash does not match its content` });
|
|
539
776
|
}
|
|
@@ -542,50 +779,10 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
|
|
|
542
779
|
} catch (err) {
|
|
543
780
|
errors.push({ code: err.code ?? "E_EVENT_INVALID", message: `event ${event.seq} (${event.event}): ${err.message}` });
|
|
544
781
|
}
|
|
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
|
-
}
|
|
782
|
+
lastMilestone = validateMilestoneChronology(events, index, event, milestoneCounts, lastMilestone, errors);
|
|
568
783
|
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
784
|
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
|
-
}
|
|
785
|
+
validateExecutionGateChronology(events, index, seen, errors);
|
|
589
786
|
}
|
|
590
787
|
if (event.event === "VERIFICATION_RECORDED" && !seen.has("VERIFICATION_STARTED")) {
|
|
591
788
|
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "verification evidence recorded before verification started" });
|
|
@@ -620,16 +817,216 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
|
|
|
620
817
|
validateLegacyRecoveryMigrations(events, errors, {
|
|
621
818
|
allowUnmigratedLegacyRecoveryEvents: options?.allowUnmigratedLegacyRecoveryEvents === true,
|
|
622
819
|
});
|
|
623
|
-
|
|
820
|
+
errors.push(...validateCheckpointRevalidationEventBindings(events));
|
|
821
|
+
errors.push(...validateContractRevisionEventBindings(events));
|
|
822
|
+
errors.push(...validateGateSatisfactionBindings(events));
|
|
823
|
+
errors.push(...validateSemanticDecisionEventBindings(events));
|
|
824
|
+
const repairedErrors = validateContractBootstrapRepairLedger(events, errors, options);
|
|
825
|
+
return { valid: repairedErrors.length === 0, events, errors: repairedErrors };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/** Validate canonical native or non-operational interchange ledger with all domain bindings. */
|
|
829
|
+
export async function validateEventLedger(target, packageRoot, options = {}) {
|
|
830
|
+
if (await needsExistingProjectScope(target)) {
|
|
831
|
+
return withExistingProjectScope(target, () => validateEventLedger(target, packageRoot, options), { readOnly: true });
|
|
832
|
+
}
|
|
833
|
+
return validateReadLedger(target, packageRoot, options, readEvents);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Explicit legacy source validation; never makes the source writable authority. */
|
|
837
|
+
export async function validatePortableEventLedger(target, packageRoot, options = {}) {
|
|
838
|
+
return validateReadLedger(target, packageRoot, options, readPortableEvents);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async function validateReadLedger(target, packageRoot, options, reader) {
|
|
842
|
+
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
843
|
+
let events;
|
|
844
|
+
try {
|
|
845
|
+
events = await reader(target, packageRoot, { ...options, eventsPath: relPath });
|
|
846
|
+
} catch (error) {
|
|
847
|
+
// A concurrent commit can invalidate a preparation read between statements;
|
|
848
|
+
// that is the standard retryable revision conflict, not ledger evidence.
|
|
849
|
+
// Converting it into an invalid-ledger result would let classification
|
|
850
|
+
// surface a transient contention loser as permanent ownership
|
|
851
|
+
// inconsistency, so a conflict keeps its own domain error here.
|
|
852
|
+
if (error?.code === "E_STATE_REVISION_CONFLICT") throw error;
|
|
853
|
+
return { valid: false, events: [], errors: [{ code: error.code ?? "E_EVENT_INVALID", message: error.message, artifacts: [relPath] }] };
|
|
854
|
+
}
|
|
855
|
+
const result = validateLedgerEvents(events, options);
|
|
856
|
+
const bindingErrors = await validateSemanticDecisionArtifactBindings(target, packageRoot, events);
|
|
857
|
+
if (bindingErrors.length === 0) return result;
|
|
858
|
+
return { valid: false, events, errors: [...result.errors, ...bindingErrors] };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Proof reuse is minted only for one owned read-only backup callback. Public
|
|
862
|
+
// collections and caller-supplied auditSource metadata cannot opt into it.
|
|
863
|
+
const ownedLedgerProofs = new WeakMap();
|
|
864
|
+
|
|
865
|
+
function ownedSnapshotFileIdentity(filename) {
|
|
866
|
+
// The WAL is durable database content that can change while a read cursor
|
|
867
|
+
// pins the main file. The SHM sidecar only carries transient lock/index
|
|
868
|
+
// state, so it is intentionally excluded from the byte identity.
|
|
869
|
+
const paths = [filename, `${filename}-wal`];
|
|
870
|
+
const files = paths.map(current => {
|
|
871
|
+
try {
|
|
872
|
+
const file = lstatSync(current, { bigint: true });
|
|
873
|
+
if (!file.isFile()) throw protocolError("E_STATE_REVISION_CONFLICT", "Owned ledger snapshot file changed during its audit");
|
|
874
|
+
return file;
|
|
875
|
+
} catch (error) {
|
|
876
|
+
if (error?.code === "ENOENT") return null;
|
|
877
|
+
throw error;
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
const identity = files.map(file => file
|
|
881
|
+
? [file.dev, file.ino, file.size, file.mtimeNs, file.ctimeNs].join(":")
|
|
882
|
+
: "missing").join("|");
|
|
883
|
+
if (process.platform !== "win32") return identity;
|
|
884
|
+
// Windows may defer file timestamps until SQLite's open handle closes.
|
|
885
|
+
// Hash readable bytes with bounded memory so raw writes cannot reuse a proof.
|
|
886
|
+
const hash = createHash("sha256");
|
|
887
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
888
|
+
for (const [index, current] of paths.entries()) {
|
|
889
|
+
hash.update(`${index}:`);
|
|
890
|
+
if (!files[index]) continue;
|
|
891
|
+
const fd = openSync(current, "r");
|
|
892
|
+
try {
|
|
893
|
+
let count;
|
|
894
|
+
while ((count = readSync(fd, buffer, 0, buffer.length, null)) !== 0) hash.update(buffer.subarray(0, count));
|
|
895
|
+
} finally { closeSync(fd); }
|
|
896
|
+
}
|
|
897
|
+
return `${identity}:${hash.digest("hex")}`;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function assertOwnedLedgerUnchanged(events) {
|
|
901
|
+
const owned = ownedLedgerProofs.get(events);
|
|
902
|
+
if (owned && (owned.versionStatement.get().data_version !== owned.version
|
|
903
|
+
|| ownedSnapshotFileIdentity(owned.filename) !== owned.fileIdentity)) {
|
|
904
|
+
throw protocolError("E_STATE_REVISION_CONFLICT", "Owned ledger snapshot changed during its audit");
|
|
905
|
+
}
|
|
906
|
+
return owned;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function validateOwnedLedgerEvents(events, options) {
|
|
910
|
+
const owned = assertOwnedLedgerUnchanged(events);
|
|
911
|
+
if (!owned) return validateLedgerEvents(events, options);
|
|
912
|
+
const flags = {
|
|
913
|
+
allowUnmigratedLegacyRecoveryEvents: options?.allowUnmigratedLegacyRecoveryEvents === true,
|
|
914
|
+
allowUnmigratedLegacyContractBootstrapRepairMarkers: options?.allowUnmigratedLegacyContractBootstrapRepairMarkers === true,
|
|
915
|
+
};
|
|
916
|
+
const key = JSON.stringify(flags);
|
|
917
|
+
if (owned.successes.has(key)) return { valid: true, events, errors: [] };
|
|
918
|
+
const result = validateLedgerEvents(events, flags);
|
|
919
|
+
assertOwnedLedgerUnchanged(events);
|
|
920
|
+
if (result.valid) owned.successes.add(key);
|
|
921
|
+
return result;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async function evaluateCallbackLedgerAudit(target, packageRoot, options, callback, events, relPath) {
|
|
925
|
+
let result;
|
|
926
|
+
try {
|
|
927
|
+
result = validateOwnedLedgerEvents(events, options);
|
|
928
|
+
const owned = assertOwnedLedgerUnchanged(events);
|
|
929
|
+
const sameScope = owned && owned.store === getOperationalStore(target)
|
|
930
|
+
&& owned.db === owned.store.db && owned.packageRoot === packageRoot
|
|
931
|
+
&& owned.relPath === relPath && owned.taskId === options?.taskId;
|
|
932
|
+
const bindingErrors = sameScope && owned.semanticValidated
|
|
933
|
+
? [] : await validateSemanticDecisionArtifactBindings(target, packageRoot, events);
|
|
934
|
+
assertOwnedLedgerUnchanged(events);
|
|
935
|
+
if (sameScope && result.valid && bindingErrors.length === 0) owned.semanticValidated = true;
|
|
936
|
+
if (bindingErrors.length) result = { valid: false, events, errors: [...result.errors, ...bindingErrors] };
|
|
937
|
+
} catch (error) {
|
|
938
|
+
result = { valid: false, events: [], errors: [{ code: error.code ?? "E_EVENT_INVALID", message: error.message, artifacts: [relPath] }] };
|
|
939
|
+
}
|
|
940
|
+
const initiallyValid = result.valid;
|
|
941
|
+
const callbackResult = await callback(result);
|
|
942
|
+
// The caller may await other work or mutate its result object. Keep the
|
|
943
|
+
// private snapshot proof valid for the entire successful observation.
|
|
944
|
+
if (initiallyValid) assertOwnedLedgerUnchanged(events);
|
|
945
|
+
return callbackResult;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Internal callback audit: events remain owned by one immutable native snapshot. */
|
|
949
|
+
export async function withEventLedgerAudit(target, packageRoot, options, callback) {
|
|
950
|
+
if (await needsExistingProjectScope(target)) {
|
|
951
|
+
return withExistingProjectScope(target, () => withEventLedgerAudit(target, packageRoot, options, callback), { readOnly: true });
|
|
952
|
+
}
|
|
953
|
+
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
954
|
+
const store = getOperationalStore(target);
|
|
955
|
+
// Prepared mutations retain their existing read-set/CAS and staged-overlay semantics.
|
|
956
|
+
if (!store?.recognizes(relPath) || !relPath.endsWith("/events.ndjson") || store.hasPreparedMutations()) {
|
|
957
|
+
return callback(await validateEventLedger(target, packageRoot, options));
|
|
958
|
+
}
|
|
959
|
+
const taskId = store.taskId({ taskKey: relPath.split("/")[2] });
|
|
960
|
+
if (!taskId) return callback(await validateEventLedger(target, packageRoot, options));
|
|
961
|
+
if (store.auditSource?.taskId === taskId && store.auditSource.packageRoot === packageRoot && store.auditSource.relPath === relPath) {
|
|
962
|
+
return evaluateCallbackLedgerAudit(target, packageRoot, options, callback, store.auditSource.events, relPath);
|
|
963
|
+
}
|
|
964
|
+
const schema = await readSchema("event", packageRoot);
|
|
965
|
+
const { withDetachedLedgerSnapshot } = await import("../storage/ledger-event-snapshot.js");
|
|
966
|
+
const { withOperationalReadSnapshot } = await import("../storage/unit-of-work.js");
|
|
967
|
+
let completeObservation;
|
|
968
|
+
return withDetachedLedgerSnapshot(store.db, taskId, (events, db) => withOperationalReadSnapshot({ db, target }, async snapshotStore => {
|
|
969
|
+
completeObservation = snapshotStore.beginEventSnapshotObservation(relPath);
|
|
970
|
+
snapshotStore.auditSource = { taskId, packageRoot, relPath, events };
|
|
971
|
+
const filename = db.prepare("PRAGMA database_list").all().find(row => row.name === "main").file;
|
|
972
|
+
const versionStatement = db.prepare("PRAGMA data_version");
|
|
973
|
+
ownedLedgerProofs.set(events, { db, filename, store: snapshotStore, taskId, packageRoot, relPath, semanticValidated: false, fileIdentity: ownedSnapshotFileIdentity(filename),
|
|
974
|
+
versionStatement, version: versionStatement.get().data_version, successes: new Set() });
|
|
975
|
+
try { return await evaluateCallbackLedgerAudit(target, packageRoot, options, callback, events, relPath); }
|
|
976
|
+
finally { ownedLedgerProofs.delete(events); }
|
|
977
|
+
}), {
|
|
978
|
+
validate(event, index) { validateStoredEvent(event, schema, `${relPath}[${index}]`); return event; },
|
|
979
|
+
onFullScan: store.captureObservations ? digest => completeObservation(digest) : null,
|
|
980
|
+
// Keep the private schema/index proof valid for every collection read,
|
|
981
|
+
// including a cursor resumed after the caller yielded an event. The guard
|
|
982
|
+
// deliberately uses the full owned snapshot identity check on all
|
|
983
|
+
// platforms; Windows may defer filesystem timestamps, so a cheap-only
|
|
984
|
+
// resume check would expose a raw-tampered payload before the outer audit
|
|
985
|
+
// boundary noticed it.
|
|
986
|
+
guard(_events, _phase) { assertOwnedLedgerUnchanged(_events); },
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function validateContractBootstrapRepairLedger(events, errors, options) {
|
|
991
|
+
const allowUnmigrated = options?.allowUnmigratedLegacyContractBootstrapRepairMarkers === true;
|
|
992
|
+
validateContractBootstrapRepairMigrations(events, errors, {
|
|
993
|
+
allowUnmigratedLegacyContractBootstrapRepairMarkers: allowUnmigrated,
|
|
994
|
+
});
|
|
995
|
+
return repairMarkerErrors(events, errors, {
|
|
996
|
+
allowUnmigratedLegacyContractBootstrapRepairMarkers: allowUnmigrated,
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function summarizeStateEvents(state, events) {
|
|
1001
|
+
const milestones = ["EXECUTION_STARTED", "VERIFICATION_STARTED", "REVIEW_STARTED", "COMPLETION_VALIDATED"];
|
|
1002
|
+
const observed = new Set();
|
|
1003
|
+
let supportsReviewEvents = false;
|
|
1004
|
+
let latestVerification;
|
|
1005
|
+
let latestReview;
|
|
1006
|
+
let index = 0;
|
|
1007
|
+
for (const event of ledgerEventsOfTypes(events, milestones)) {
|
|
1008
|
+
if (event.taskId !== state.taskId) continue;
|
|
1009
|
+
observed.add(event.event);
|
|
1010
|
+
if (event.event === "VERIFICATION_STARTED") {
|
|
1011
|
+
latestVerification = { event, index };
|
|
1012
|
+
if (Number.isInteger(event.details?.verificationCycle)) supportsReviewEvents = true;
|
|
1013
|
+
}
|
|
1014
|
+
if (event.event === "REVIEW_STARTED") {
|
|
1015
|
+
supportsReviewEvents = true;
|
|
1016
|
+
latestReview = { event, index };
|
|
1017
|
+
}
|
|
1018
|
+
index += 1;
|
|
1019
|
+
}
|
|
1020
|
+
return { observed, supportsReviewEvents, latestVerification, latestReview };
|
|
624
1021
|
}
|
|
625
1022
|
|
|
626
1023
|
export function validateStateLedgerCoherence(state, events) {
|
|
627
|
-
const errors = [
|
|
1024
|
+
const errors = [
|
|
1025
|
+
...validateCheckpointRevalidationCurrentBinding(state, events),
|
|
1026
|
+
...validateContractRevisionCurrentBinding(state, events),
|
|
1027
|
+
];
|
|
628
1028
|
if (!Number.isInteger(state.verificationCycle)) return errors;
|
|
629
|
-
const
|
|
630
|
-
const observed = new Set(taskEvents.map((event) => event.event));
|
|
631
|
-
const supportsReviewEvents = taskEvents.some((event) => event.event === "REVIEW_STARTED"
|
|
632
|
-
|| (event.event === "VERIFICATION_STARTED" && Number.isInteger(event.details?.verificationCycle)));
|
|
1029
|
+
const { observed, supportsReviewEvents, latestVerification, latestReview } = summarizeStateEvents(state, events);
|
|
633
1030
|
const phaseRequirements = {
|
|
634
1031
|
EXECUTING: ["EXECUTION_STARTED"],
|
|
635
1032
|
VERIFYING: ["EXECUTION_STARTED", "VERIFICATION_STARTED"],
|
|
@@ -647,21 +1044,12 @@ export function validateStateLedgerCoherence(state, events) {
|
|
|
647
1044
|
}
|
|
648
1045
|
}
|
|
649
1046
|
if (Number.isInteger(state.verificationCycle)) {
|
|
650
|
-
|
|
651
|
-
.map((event, index) => ({ event, index }))
|
|
652
|
-
.filter(({ event }) => event.event === "VERIFICATION_STARTED");
|
|
653
|
-
const cycles = verificationEvents.map(({ event }) => event.details?.verificationCycle ?? 1);
|
|
654
|
-
if (cycles.at(-1) !== state.verificationCycle) {
|
|
1047
|
+
if ((latestVerification ? latestVerification.event.details?.verificationCycle ?? 1 : undefined) !== state.verificationCycle) {
|
|
655
1048
|
errors.push({
|
|
656
1049
|
code: "E_STATE_LEDGER_DIVERGENCE",
|
|
657
1050
|
message: "Work-state verification cycle does not match the lifecycle ledger",
|
|
658
1051
|
});
|
|
659
1052
|
}
|
|
660
|
-
const latestVerification = verificationEvents.at(-1);
|
|
661
|
-
const latestReview = taskEvents
|
|
662
|
-
.map((event, index) => ({ event, index }))
|
|
663
|
-
.filter(({ event }) => event.event === "REVIEW_STARTED")
|
|
664
|
-
.at(-1);
|
|
665
1053
|
const latestReviewCycle = latestReview?.event.details?.verificationCycle;
|
|
666
1054
|
if (state.phase === "VERIFYING"
|
|
667
1055
|
&& latestReviewCycle === state.verificationCycle
|