@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
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, open, opendir, readdir, rename } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { digestFile, syncDirectory } from "./file-durability.js";
|
|
7
|
+
import { assertSafePath, writeFileAtomic } from "../core/filesystem.js";
|
|
8
|
+
import { findIncompleteTransactions } from "../core/transaction.js";
|
|
9
|
+
import { assertOwnedStorageMaintenance, withStorageMaintenance } from "./maintenance.js";
|
|
10
|
+
import { readStorageMetadataJson, STORAGE_CATALOG_LIMITS } from "./metadata-json.js";
|
|
11
|
+
import { readStorageVersionMarker } from "./storage-marker.js";
|
|
12
|
+
import { LEGACY_TASK_ARTIFACT_PATHS } from "../core/task-paths.js";
|
|
13
|
+
import { assertNoLegacyOperationLocks } from "./legacy-maintenance-boundary.js";
|
|
14
|
+
|
|
15
|
+
export const LEGACY_SOURCE_ROOTS = Object.freeze([...new Set([
|
|
16
|
+
...Object.values(LEGACY_TASK_ARTIFACT_PATHS),
|
|
17
|
+
".forgeloop/task-state", ".forgeloop/events.ndjson.index.json",
|
|
18
|
+
".forgeloop/sessions", ".forgeloop/.txn", ".forgeloop/attachments",
|
|
19
|
+
"export-index.json",
|
|
20
|
+
])]);
|
|
21
|
+
const SOURCE_ROOTS = LEGACY_SOURCE_ROOTS;
|
|
22
|
+
|
|
23
|
+
function failure(code, message) { return Object.assign(new Error(message), { code }); }
|
|
24
|
+
|
|
25
|
+
async function infoIfPresent(filename) {
|
|
26
|
+
try { return await lstat(filename); }
|
|
27
|
+
catch (error) { if (error.code === "ENOENT") return null; throw error; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function syncDirectories(directory) {
|
|
31
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
32
|
+
if (entry.isDirectory()) await syncDirectories(path.join(directory, entry.name));
|
|
33
|
+
}
|
|
34
|
+
await syncDirectory(directory);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function recordInventoryEntry(entries, value, budget) {
|
|
38
|
+
const encoded = JSON.stringify(value);
|
|
39
|
+
// Reserve pretty-print overhead and manifest status/error metadata as well
|
|
40
|
+
// as encoded entry bytes. Refuse before retaining an oversized entry.
|
|
41
|
+
const size = Buffer.byteLength(encoded) + 128;
|
|
42
|
+
if (entries.length >= STORAGE_CATALOG_LIMITS.maxArrayLength || budget.bytes + size > STORAGE_CATALOG_LIMITS.maxBytes) {
|
|
43
|
+
throw failure("E_STORAGE_MIGRATION_SOURCE_INVALID", "Source inventory exceeds the retained manifest limits");
|
|
44
|
+
}
|
|
45
|
+
budget.bytes += size;
|
|
46
|
+
entries.push(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function inventoryPath(target, relativePath, files, directories, budget) {
|
|
50
|
+
const filename = await assertSafePath(target, relativePath);
|
|
51
|
+
const info = await infoIfPresent(filename);
|
|
52
|
+
if (!info) return;
|
|
53
|
+
if (info.isDirectory()) {
|
|
54
|
+
recordInventoryEntry(directories, relativePath, budget);
|
|
55
|
+
for await (const entry of await opendir(filename)) {
|
|
56
|
+
await inventoryPath(target, `${relativePath}/${entry.name}`, files, directories, budget);
|
|
57
|
+
}
|
|
58
|
+
} else if (info.isFile()) {
|
|
59
|
+
recordInventoryEntry(files, { path: relativePath, ...await digestFile(filename, { includeSize: true }) }, budget);
|
|
60
|
+
} else {
|
|
61
|
+
throw failure("E_STORAGE_MIGRATION_SOURCE_INVALID", `Unsupported source entry: ${relativePath}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Shared bounded inventory for fixed roots selected by storage maintenance. */
|
|
66
|
+
export async function inventoryStorageRoots(target, roots) {
|
|
67
|
+
const files = [];
|
|
68
|
+
const directories = [];
|
|
69
|
+
const budget = { bytes: 4096 };
|
|
70
|
+
for (const root of roots) await inventoryPath(target, root, files, directories, budget);
|
|
71
|
+
return { files: files.sort((a, b) => a.path.localeCompare(b.path)), directories: directories.sort() };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function inventoryLegacySourceLayout(target) {
|
|
75
|
+
return inventoryStorageRoots(target, SOURCE_ROOTS);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** An archive is owned source storage: unexpected entries cannot be ignored. */
|
|
79
|
+
export async function inventoryLegacyArchiveLayout(target) {
|
|
80
|
+
if (!(await infoIfPresent(target))) return { files: [], directories: [] };
|
|
81
|
+
const root = await assertSafePath(target, ".");
|
|
82
|
+
const files = [];
|
|
83
|
+
const directories = [];
|
|
84
|
+
const budget = { bytes: 4096 };
|
|
85
|
+
for await (const entry of await opendir(root)) await inventoryPath(target, entry.name, files, directories, budget);
|
|
86
|
+
const belongs = (name, directory) => SOURCE_ROOTS.some(source => name === source || name.startsWith(`${source}/`) || (directory && source.startsWith(`${name}/`)));
|
|
87
|
+
if (files.some(file => !belongs(file.path, false)) || directories.some(directory => !belongs(directory, true))) {
|
|
88
|
+
throw failure("E_STORAGE_MIGRATION_ARCHIVE_INVALID", "Archive contains entries outside the captured source roots");
|
|
89
|
+
}
|
|
90
|
+
return { files: files.sort((a, b) => a.path.localeCompare(b.path)), directories: directories.sort() };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function inventoryLegacySource(target) {
|
|
94
|
+
return (await inventoryLegacySourceLayout(target)).files;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function verifyLegacySourceCapture(target, destination) {
|
|
98
|
+
const retained = await assertSafePath(target, destination);
|
|
99
|
+
const filename = await assertSafePath(retained, "source-manifest.json");
|
|
100
|
+
const manifest = await readCaptureManifest(filename);
|
|
101
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)
|
|
102
|
+
|| manifest.schemaVersion !== 2 || manifest.kind !== "LEGACY_SOURCE_CAPTURE"
|
|
103
|
+
|| manifest.status !== "CAPTURED" || manifest.writersQuiesced !== true || !Array.isArray(manifest.files) || !Array.isArray(manifest.directories)) {
|
|
104
|
+
throw failure("E_STORAGE_MIGRATION_CAPTURE_INVALID", "Capture manifest is incomplete or unsupported");
|
|
105
|
+
}
|
|
106
|
+
const source = await assertSafePath(retained, "source");
|
|
107
|
+
if (JSON.stringify(await inventoryRetainedSource(source)) !== JSON.stringify({ files: manifest.files, directories: manifest.directories })) {
|
|
108
|
+
throw failure("E_STORAGE_MIGRATION_CAPTURE_INVALID", "Retained source membership or digests disagree with capture manifest");
|
|
109
|
+
}
|
|
110
|
+
return { path: retained, source, manifest };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function assertLegacyIdle(target) {
|
|
114
|
+
if (await infoIfPresent(await assertSafePath(target, ".forgeloop/state.sqlite"))) {
|
|
115
|
+
throw failure("E_STORAGE_MIGRATION_SOURCE_INVALID", "Legacy capture cannot replace SQLite snapshot backup");
|
|
116
|
+
}
|
|
117
|
+
await assertNoLegacyOperationLocks(target, { code: "E_STORAGE_MIGRATION_BUSY" });
|
|
118
|
+
await assertSafePath(target, ".forgeloop/.txn");
|
|
119
|
+
if ((await findIncompleteTransactions(target)).length) {
|
|
120
|
+
throw failure("E_STORAGE_MIGRATION_SOURCE_INVALID", "Incomplete transactions require supported recovery before capture");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Retained source capture only. Operator quiescence is necessary, not inferred. */
|
|
125
|
+
export async function captureLegacySource(target, destination, { writersQuiesced = false } = {}) {
|
|
126
|
+
if (writersQuiesced !== true) throw failure("E_STORAGE_MIGRATION_QUIESCENCE_REQUIRED", "Stop and exclude all legacy writers before capture");
|
|
127
|
+
return withStorageMaintenance(target, () => captureExcludedLegacySource(target, destination));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function captureExcludedLegacySource(target, destination) {
|
|
131
|
+
await assertLegacyIdle(target);
|
|
132
|
+
const retained = await assertSafePath(target, destination);
|
|
133
|
+
// Destination must not overlap any source root in either direction.
|
|
134
|
+
const normalized = path.relative(path.resolve(target), path.resolve(retained)).split(path.sep).join("/");
|
|
135
|
+
if (!normalized || SOURCE_ROOTS.some(root => normalized === root || normalized.startsWith(`${root}/`) || root.startsWith(`${normalized}/`))) {
|
|
136
|
+
throw failure("E_STORAGE_MIGRATION_DESTINATION_INVALID", "Capture destination overlaps retained operational sources");
|
|
137
|
+
}
|
|
138
|
+
const inventory = await inventoryLegacySourceLayout(target);
|
|
139
|
+
const { files, directories } = inventory;
|
|
140
|
+
await mkdir(path.dirname(retained), { recursive: true });
|
|
141
|
+
await mkdir(retained); // Exclusive: never replace or reuse an earlier capture.
|
|
142
|
+
const source = path.join(retained, "source");
|
|
143
|
+
await mkdir(source);
|
|
144
|
+
const manifestPath = path.join(retained, "source-manifest.json");
|
|
145
|
+
const manifest = { schemaVersion: 2, kind: "LEGACY_SOURCE_CAPTURE", status: "CAPTURING", writersQuiesced: true, files, directories };
|
|
146
|
+
const persist = () => writeFileAtomic(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
147
|
+
await persist();
|
|
148
|
+
try {
|
|
149
|
+
for (const directory of directories) await mkdir(await assertSafePath(source, directory), { recursive: true });
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
await copyCapturedFile(target, source, file);
|
|
152
|
+
}
|
|
153
|
+
await assertLegacyIdle(target);
|
|
154
|
+
if (JSON.stringify(await inventoryLegacySourceLayout(target)) !== JSON.stringify(inventory)) {
|
|
155
|
+
throw failure("E_STORAGE_MIGRATION_SOURCE_CHANGED", "Source membership or bytes changed during capture");
|
|
156
|
+
}
|
|
157
|
+
await syncDirectories(source);
|
|
158
|
+
manifest.status = "CAPTURED";
|
|
159
|
+
await persist();
|
|
160
|
+
return { path: retained, source, manifest };
|
|
161
|
+
} catch (error) {
|
|
162
|
+
manifest.status = "FAILED";
|
|
163
|
+
manifest.error = { code: error.code ?? "E_STORAGE_MIGRATION_SOURCE_INVALID", message: error.message };
|
|
164
|
+
try { await persist(); } catch { /* Retain CAPTURING if failure metadata cannot be persisted. */ }
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function readCaptureManifest(filename) {
|
|
170
|
+
try { return await readStorageMetadataJson(path.dirname(filename), path.basename(filename)); }
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (["JSON_LIMIT_EXCEEDED", "E_STORAGE_METADATA_INVALID"].includes(error.code)) throw failure("E_STORAGE_MIGRATION_CAPTURE_INVALID", error.message);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function inventoryRetainedSource(source) {
|
|
178
|
+
let inventory;
|
|
179
|
+
try { inventory = await inventoryLegacyArchiveLayout(source); }
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error.code === "E_STORAGE_MIGRATION_ARCHIVE_INVALID") throw failure("E_STORAGE_MIGRATION_CAPTURE_INVALID", error.message);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
return { files: inventory.files, directories: inventory.directories.filter(name => SOURCE_ROOTS.some(root => name === root || name.startsWith(`${root}/`))) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function copyCapturedFile(target, source, file) {
|
|
188
|
+
const copied = await assertSafePath(source, file.path);
|
|
189
|
+
await mkdir(path.dirname(copied), { recursive: true });
|
|
190
|
+
await pipeline(createReadStream(await assertSafePath(target, file.path)), createWriteStream(copied, { flags: "wx", mode: 0o600 }));
|
|
191
|
+
const handle = await open(copied, "r+");
|
|
192
|
+
try { await handle.sync(); } finally { await handle.close(); }
|
|
193
|
+
const copiedDigest = await digestFile(copied, { includeSize: true });
|
|
194
|
+
if (copiedDigest.size !== file.size || copiedDigest.sha256 !== file.sha256) throw failure("E_STORAGE_MIGRATION_SOURCE_CHANGED", `Source bytes changed during recovery: ${file.path}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Complete recorded capture only while the original source inventory is unchanged. */
|
|
198
|
+
export async function resumeLegacySourceCapture(target, destination, { writersQuiesced = false } = {}) {
|
|
199
|
+
if (writersQuiesced !== true) throw failure("E_STORAGE_MIGRATION_QUIESCENCE_REQUIRED", "Capture recovery requires excluded writers");
|
|
200
|
+
await assertOwnedStorageMaintenance(target);
|
|
201
|
+
const retained = await assertSafePath(target, destination);
|
|
202
|
+
const manifestPath = await assertSafePath(retained, "source-manifest.json");
|
|
203
|
+
const manifest = await readCaptureManifest(manifestPath);
|
|
204
|
+
if (manifest?.status === "CAPTURED") return verifyLegacySourceCapture(target, destination);
|
|
205
|
+
if (manifest?.schemaVersion !== 2 || manifest.kind !== "LEGACY_SOURCE_CAPTURE" || !["CAPTURING", "FAILED"].includes(manifest.status)
|
|
206
|
+
|| manifest.writersQuiesced !== true || !Array.isArray(manifest.files) || !Array.isArray(manifest.directories)) throw failure("E_STORAGE_MIGRATION_CAPTURE_INVALID", "Capture recovery requires a recorded source inventory");
|
|
207
|
+
await assertLegacyIdle(target);
|
|
208
|
+
if (await readStorageVersionMarker(target)) throw failure("E_STORAGE_MIGRATION_CAPTURE_INVALID", "Capture cannot resume after cutover begins");
|
|
209
|
+
for (const name of ["candidate-manifest.json", "candidate.sqlite", "publication"]) {
|
|
210
|
+
if (await infoIfPresent(await assertSafePath(retained, name))) throw failure("E_STORAGE_MIGRATION_CAPTURE_INVALID", "Capture cannot resume after candidate preparation begins");
|
|
211
|
+
}
|
|
212
|
+
const expected = { files: manifest.files, directories: manifest.directories };
|
|
213
|
+
if (JSON.stringify(await inventoryLegacySourceLayout(target)) !== JSON.stringify(expected)) throw failure("E_STORAGE_MIGRATION_SOURCE_CHANGED", "Original source inventory changed after capture began");
|
|
214
|
+
const source = await assertSafePath(retained, "source");
|
|
215
|
+
const partial = await inventoryRetainedSource(source);
|
|
216
|
+
const expectedPaths = new Set(manifest.files.map(file => file.path));
|
|
217
|
+
const expectedDirectories = new Set(manifest.directories);
|
|
218
|
+
if (partial.files.some(file => !expectedPaths.has(file.path)) || partial.directories.some(name => !expectedDirectories.has(name))) throw failure("E_STORAGE_MIGRATION_CAPTURE_INVALID", "Partial capture contains unrecorded entries");
|
|
219
|
+
const history = await assertSafePath(retained, "source-history");
|
|
220
|
+
await mkdir(history, { recursive: true });
|
|
221
|
+
const attempt = await assertSafePath(history, randomUUID());
|
|
222
|
+
await mkdir(attempt);
|
|
223
|
+
await writeFileAtomic(await assertSafePath(attempt, "source-manifest.json"), `${JSON.stringify(manifest)}\n`);
|
|
224
|
+
await syncDirectory(retained); await syncDirectory(history); await syncDirectory(attempt);
|
|
225
|
+
await mkdir(source, { recursive: true });
|
|
226
|
+
for (const name of manifest.directories) await mkdir(await assertSafePath(source, name), { recursive: true });
|
|
227
|
+
await completeCaptureFiles(target, source, attempt, manifest.files, partial.files);
|
|
228
|
+
await assertLegacyIdle(target);
|
|
229
|
+
if (JSON.stringify(await inventoryLegacySourceLayout(target)) !== JSON.stringify(expected)
|
|
230
|
+
|| JSON.stringify(await inventoryRetainedSource(source)) !== JSON.stringify(expected)) throw failure("E_STORAGE_MIGRATION_SOURCE_CHANGED", "Source or capture changed during recovery");
|
|
231
|
+
await syncDirectories(source);
|
|
232
|
+
await assertOwnedStorageMaintenance(target);
|
|
233
|
+
delete manifest.error;
|
|
234
|
+
manifest.status = "CAPTURED";
|
|
235
|
+
await writeFileAtomic(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
236
|
+
return verifyLegacySourceCapture(target, destination);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function completeCaptureFiles(target, source, attempt, files, partialFiles) {
|
|
240
|
+
const current = new Map(partialFiles.map(file => [file.path, file]));
|
|
241
|
+
for (const file of files) {
|
|
242
|
+
const previous = current.get(file.path);
|
|
243
|
+
if (previous?.size === file.size && previous.sha256 === file.sha256) continue;
|
|
244
|
+
if (previous) {
|
|
245
|
+
await assertOwnedStorageMaintenance(target);
|
|
246
|
+
const forensic = await assertSafePath(attempt, `partial/${file.path}`);
|
|
247
|
+
await mkdir(path.dirname(forensic), { recursive: true });
|
|
248
|
+
await syncDirectories(attempt);
|
|
249
|
+
await rename(await assertSafePath(source, file.path), forensic);
|
|
250
|
+
await syncDirectory(path.dirname(forensic));
|
|
251
|
+
await syncDirectory(path.dirname(await assertSafePath(source, file.path)));
|
|
252
|
+
}
|
|
253
|
+
await copyCapturedFile(target, source, file);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { assertSafePath } from "../core/filesystem.js";
|
|
2
|
+
import { getPackageRoot } from "../core/templates.js";
|
|
3
|
+
import { assertOwnedStorageMaintenance } from "./maintenance.js";
|
|
4
|
+
import { verifyMigrationPublicationStage } from "./migration-publication.js";
|
|
5
|
+
import { readStorageVersionMarker } from "./storage-marker.js";
|
|
6
|
+
import { checkStorageIntegrity, openStorageDatabase, readStorageMeta } from "./connection.js";
|
|
7
|
+
import { withStorageSnapshot } from "./snapshot.js";
|
|
8
|
+
import { validateMigrationDatabase } from "./migration-validation.js";
|
|
9
|
+
import { iterateAttachmentReferences } from "./attachment-references.js";
|
|
10
|
+
import { verifyAttachmentFile } from "./attachment-files.js";
|
|
11
|
+
import { assertSnapshotContinuity } from "./snapshot-continuity.js";
|
|
12
|
+
|
|
13
|
+
function invalid(message) { return Object.assign(new Error(message), { code: "E_STORAGE_MIGRATION_TERMINAL_INVALID" }); }
|
|
14
|
+
|
|
15
|
+
/** Verify current state after publication; never write or restore an old snapshot. */
|
|
16
|
+
export async function verifyPublishedMigration(target, destination, { writersQuiesced = false, packageRoot = getPackageRoot() } = {}) {
|
|
17
|
+
if (writersQuiesced !== true) throw invalid("Terminal verification requires excluded writers");
|
|
18
|
+
await assertOwnedStorageMaintenance(target);
|
|
19
|
+
const staged = await verifyMigrationPublicationStage(target, destination, { packageRoot, sourcePartition: true, allowPublished: true });
|
|
20
|
+
if (staged.journal.phase !== "PUBLISHED") throw invalid("Publication journal is not terminal");
|
|
21
|
+
const marker = await readStorageVersionMarker(target);
|
|
22
|
+
if (!marker || marker.phase !== "ACTIVE" || marker.operationId !== staged.journal.operationId
|
|
23
|
+
|| marker.sourceInventoryFingerprint !== staged.journal.sourceInventoryFingerprint) throw invalid("Active marker does not match terminal publication");
|
|
24
|
+
for (const suffix of ["", "-wal", "-shm"]) await assertSafePath(target, `.forgeloop/state.sqlite${suffix}`);
|
|
25
|
+
const db = openStorageDatabase(await assertSafePath(target, ".forgeloop/state.sqlite"), { readOnly: true });
|
|
26
|
+
let original;
|
|
27
|
+
try {
|
|
28
|
+
original = openStorageDatabase(await assertSafePath(staged.bundle, "state.sqlite"), { readOnly: true });
|
|
29
|
+
return await withStorageSnapshot(db, async current => {
|
|
30
|
+
if (!checkStorageIntegrity(current).ok || readStorageMeta(current).schema_version !== marker.databaseSchemaVersion) throw invalid("Current storage integrity or version differs");
|
|
31
|
+
assertSnapshotContinuity(original, current, invalid);
|
|
32
|
+
const validation = await validateMigrationDatabase(current, { target, packageRoot });
|
|
33
|
+
let attachments = 0;
|
|
34
|
+
for (const reference of iterateAttachmentReferences(current)) { await verifyAttachmentFile(target, reference); attachments += 1; }
|
|
35
|
+
await assertOwnedStorageMaintenance(target);
|
|
36
|
+
return { phase: "PUBLISHED", operationId: marker.operationId, taskCount: validation.tasks.length, attachments, currentStateVerified: true, restored: false };
|
|
37
|
+
});
|
|
38
|
+
} finally { try { original?.close(); } finally { db.close(); } }
|
|
39
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { assertSchema, readSchema } from "../core/schema-validation.js";
|
|
3
|
+
import { assertJsonBytes, assertJsonLimits } from "../core/json-safety.js";
|
|
4
|
+
import { validateTaskDescriptor } from "../core/task-descriptor.js";
|
|
5
|
+
import { validateContract } from "../core/contract.js";
|
|
6
|
+
import { validateCodeManifest } from "../core/code-manifest.js";
|
|
7
|
+
import { validateActionArtifact, validateApprovalArtifact } from "../core/action-model.js";
|
|
8
|
+
import { validateEventLedger, validateStateLedgerCoherence } from "../core/events.js";
|
|
9
|
+
import { readWorkState } from "../core/work-state.js";
|
|
10
|
+
import { validateCheckExecutionProvenance } from "../core/completion-artifacts.js";
|
|
11
|
+
import { withOperationalStore } from "./unit-of-work.js";
|
|
12
|
+
|
|
13
|
+
const ARTIFACT_SCHEMAS = Object.freeze({
|
|
14
|
+
contract: "current-contract", route: "routing-result", preflight: "preflight",
|
|
15
|
+
continuity: "continuity", receipt: "execution-receipt", policySnapshot: "policy-snapshot",
|
|
16
|
+
recovery: "task-recovery", workspaceBinding: "workspace-binding", responsibility: "responsibility",
|
|
17
|
+
verificationScope: "verification-scope", usage: "usage", testUtility: "test-utility",
|
|
18
|
+
gate: "gate", handoff: "handoff-envelope", decision: "semantic-decision",
|
|
19
|
+
evaluation: "trajectory-evaluation", structuralQuality: "structural-quality",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function artifactSchema(row) {
|
|
23
|
+
if (row.kind !== "attestation") return ARTIFACT_SCHEMAS[row.kind];
|
|
24
|
+
const name = path.posix.basename(row.artifact_id);
|
|
25
|
+
return name === "code-manifest" ? "code-manifest" : name === "statement" ? "in-toto-statement" : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Validate imported payloads with their existing protocol schemas and rules. */
|
|
29
|
+
export async function validateMigrationDatabase(db, { target, packageRoot }) {
|
|
30
|
+
const schemas = new Map();
|
|
31
|
+
const validate = async (text, name, label, taskId = null) => {
|
|
32
|
+
if (!name) throw Object.assign(new Error(`Unmapped operational artifact: ${label}`), { code: "E_STORAGE_MIGRATION_UNMAPPED_SOURCE" });
|
|
33
|
+
assertJsonBytes(text, label);
|
|
34
|
+
const value = JSON.parse(text);
|
|
35
|
+
assertJsonLimits(value, label);
|
|
36
|
+
if (!schemas.has(name)) schemas.set(name, await readSchema(name, packageRoot));
|
|
37
|
+
assertSchema(value, schemas.get(name), label);
|
|
38
|
+
if (taskId && value.taskId !== undefined && value.taskId !== taskId) {
|
|
39
|
+
throw Object.assign(new Error(`Artifact identity differs from its task: ${label}`), { code: "E_STORAGE_PAYLOAD_MISMATCH" });
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
for (const row of db.prepare("SELECT task_id, descriptor_json FROM tasks ORDER BY task_id").iterate()) {
|
|
44
|
+
await validateTaskDescriptor(await validate(row.descriptor_json, "task-descriptor", row.task_id, row.task_id), packageRoot);
|
|
45
|
+
}
|
|
46
|
+
for (const row of db.prepare("SELECT task_id, kind, artifact_id, payload_json FROM task_artifacts ORDER BY task_id, kind, artifact_id").iterate()) {
|
|
47
|
+
const name = artifactSchema(row);
|
|
48
|
+
const value = await validate(row.payload_json, name, `${row.task_id}/${row.kind}/${row.artifact_id}`, row.task_id);
|
|
49
|
+
if (row.kind === "contract") await validateContract(value, packageRoot);
|
|
50
|
+
if (name === "code-manifest") await validateCodeManifest(value, packageRoot);
|
|
51
|
+
}
|
|
52
|
+
for (const [table, name, validator] of [["actions", "action", validateActionArtifact], ["approvals", "approval", validateApprovalArtifact], ["executions", "execution", null]]) {
|
|
53
|
+
for (const row of db.prepare(`SELECT task_id, payload_json FROM ${table} ORDER BY task_id, payload_json`).iterate()) {
|
|
54
|
+
const value = await validate(row.payload_json, name, table, row.task_id);
|
|
55
|
+
validator?.(value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return withOperationalStore({ db, target }, async () => {
|
|
59
|
+
const tasks = [];
|
|
60
|
+
for (const row of db.prepare("SELECT task_id FROM tasks ORDER BY task_id").iterate()) {
|
|
61
|
+
const state = await readWorkState(target, { taskId: row.task_id, packageRoot });
|
|
62
|
+
for (const check of state?.checks ?? []) {
|
|
63
|
+
if (check.provenance === "FORGELOOP_EXECUTED") {
|
|
64
|
+
await validateCheckExecutionProvenance(check, { target, packageRoot, taskId: row.task_id });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId: row.task_id });
|
|
68
|
+
const errors = [...ledger.errors, ...(state ? validateStateLedgerCoherence(state, ledger.events) : [])];
|
|
69
|
+
if (errors.length) throw Object.assign(new Error(`Imported task failed domain validation: ${row.task_id}`), { code: "E_STORAGE_MIGRATION_VALIDATION_FAILED", errors });
|
|
70
|
+
const head = ledger.events.at(-1);
|
|
71
|
+
tasks.push({ taskId: row.task_id, phase: state?.phase ?? null, revision: state?.revision ?? null, events: ledger.events.length, head: head ? { seq: head.seq, hash: head.hash } : null });
|
|
72
|
+
}
|
|
73
|
+
return { tasks, schemas: [...schemas.keys()].sort() };
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { resumeStorageMaintenance, withStorageMaintenance } from "./maintenance.js";
|
|
2
|
+
import { resumeLegacySourceCapture } from "./migration-source.js";
|
|
3
|
+
import { prepareMigrationCandidate, resumeMigrationCandidate } from "./migration-candidate.js";
|
|
4
|
+
import { stageMigrationPublication } from "./migration-publication.js";
|
|
5
|
+
import { archiveMigrationSources } from "./migration-archive.js";
|
|
6
|
+
import { activateArchivedMigration } from "./migration-activate.js";
|
|
7
|
+
import { verifyPublishedMigration } from "./migration-terminal.js";
|
|
8
|
+
|
|
9
|
+
/** Initial cutover only. Failed attempts retain exclusion and evidence for recovery. */
|
|
10
|
+
export async function migrateProjectStorage(target, { destination, writersQuiesced = false, packageRoot } = {}) {
|
|
11
|
+
if (typeof destination !== "string" || !destination || writersQuiesced !== true) {
|
|
12
|
+
throw Object.assign(new Error("storage-migrate requires --destination and explicit --writers-quiesced"), { code: "E_CLI_INVOCATION_INVALID" });
|
|
13
|
+
}
|
|
14
|
+
return withStorageMaintenance(target, async () => {
|
|
15
|
+
const options = { writersQuiesced, packageRoot };
|
|
16
|
+
await prepareMigrationCandidate(target, { destination, ...options });
|
|
17
|
+
await stageMigrationPublication(target, destination, options);
|
|
18
|
+
await archiveMigrationSources(target, destination, options);
|
|
19
|
+
const activated = await activateArchivedMigration(target, destination, options);
|
|
20
|
+
const verified = await verifyPublishedMigration(target, destination, options);
|
|
21
|
+
return { ...verified, path: activated.path, destination };
|
|
22
|
+
}, { retainOnError: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/** Resume retained publication, rebuilding only verified unpublished staging. */
|
|
27
|
+
export async function resumeProjectStorageMigration(target, { destination, expectedOwnerId, writersQuiesced = false, acknowledgeUnboundHandoff = false, packageRoot } = {}) {
|
|
28
|
+
if (typeof destination !== "string" || !destination) {
|
|
29
|
+
throw Object.assign(new Error("Migration resume requires the retained destination"), { code: "E_CLI_INVOCATION_INVALID" });
|
|
30
|
+
}
|
|
31
|
+
return resumeStorageMaintenance(target, { expectedOwnerId, writersQuiesced, acknowledgeUnboundHandoff }, async () => {
|
|
32
|
+
const options = { writersQuiesced, packageRoot };
|
|
33
|
+
await resumeLegacySourceCapture(target, destination, options);
|
|
34
|
+
await resumeMigrationCandidate(target, destination, options);
|
|
35
|
+
const { resumeMigrationPublicationStage } = await import("./migration-publication.js");
|
|
36
|
+
const staged = await resumeMigrationPublicationStage(target, destination, options);
|
|
37
|
+
if (staged.journal.phase !== "PUBLISHED") {
|
|
38
|
+
if (["STAGED", "ARCHIVING"].includes(staged.journal.phase)) await archiveMigrationSources(target, destination, options);
|
|
39
|
+
await activateArchivedMigration(target, destination, options);
|
|
40
|
+
}
|
|
41
|
+
return { ...await verifyPublishedMigration(target, destination, options), destination };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { needsExistingProjectScope, withExistingProjectScope } from "./existing-project-scope.js";
|
|
2
|
+
import { getOperationalStore } from "./operational-context.js";
|
|
3
|
+
import { validateAttachmentReference } from "./attachment-binding.js";
|
|
4
|
+
import { withVerifiedAttachmentFile } from "./attachment-files.js";
|
|
5
|
+
import { ensureWithin } from "../core/filesystem.js";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
function isObjectAddress(target, relativePath) {
|
|
9
|
+
const portable = relativePath.replaceAll("\\", "/");
|
|
10
|
+
const normalizeNames = value => value.replaceAll("\\", "/").split("/")
|
|
11
|
+
.map(segment => segment.replace(/[. ]+$/, "").split(":")[0].toLowerCase()).join("/");
|
|
12
|
+
const prefix = ".forgeloop/attachments/objects/";
|
|
13
|
+
// Keep lexical object traversal protected, and detect absolute/dot aliases.
|
|
14
|
+
// Classification never rewrites the immutable SQL path binding.
|
|
15
|
+
return normalizeNames(portable).startsWith(prefix)
|
|
16
|
+
|| normalizeNames(path.relative(target, path.resolve(target, portable))).startsWith(prefix);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Explicit non-object paths retain legacy handling until their migration. */
|
|
20
|
+
export async function withOperationalAttachmentFile(target, relativePath, taskId, callback) {
|
|
21
|
+
if (await needsExistingProjectScope(target)) {
|
|
22
|
+
return withExistingProjectScope(target, () => withOperationalAttachmentFile(target, relativePath, taskId, callback), { readOnly: true });
|
|
23
|
+
}
|
|
24
|
+
const store = getOperationalStore(target);
|
|
25
|
+
if (!store || !isObjectAddress(target, relativePath)) return callback(ensureWithin(target, relativePath));
|
|
26
|
+
const invalid = message => Object.assign(new Error(message), { code: "E_STORAGE_ATTACHMENT_INVALID" });
|
|
27
|
+
if (store.db.isTransaction) throw invalid("External attachment consumption cannot run inside the native writer");
|
|
28
|
+
const row = store.observe(`attachment-path:${JSON.stringify([taskId, relativePath])}`, () => store.db.prepare(
|
|
29
|
+
"SELECT * FROM attachment_references WHERE task_id = ? AND path = ? ORDER BY reference_id LIMIT 1",
|
|
30
|
+
).get(taskId, relativePath) ?? null);
|
|
31
|
+
let reference = row ? validateAttachmentReference({ taskId: row.task_id, referenceId: row.reference_id, path: row.path, size: row.size, sha256: row.sha256 }) : null;
|
|
32
|
+
if (!reference) {
|
|
33
|
+
for (const value of store.attachments.values()) {
|
|
34
|
+
if (value.taskId === taskId && value.path === relativePath) { reference = value; break; }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!reference) throw invalid("Attachment path has no binding for this task");
|
|
38
|
+
let consumerStarted = false;
|
|
39
|
+
try {
|
|
40
|
+
return await withVerifiedAttachmentFile(target, validateAttachmentReference(reference), filename => {
|
|
41
|
+
consumerStarted = true;
|
|
42
|
+
return callback(filename);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!consumerStarted && ["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code)) throw invalid("Referenced attachment bytes are missing or unsafe");
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
// Per-command storage scope selected at the project boundary. Actor command
|
|
6
|
+
// input cannot select a different backend or connection.
|
|
7
|
+
export const operationalContext = new AsyncLocalStorage();
|
|
8
|
+
|
|
9
|
+
export function assertCanonicalPersistence(persistence) {
|
|
10
|
+
if (persistence !== null && persistence !== undefined) {
|
|
11
|
+
throw Object.assign(new Error("Temporary command persistence capabilities are retired; use canonical project storage"), { code: "E_STORAGE_OPERATION_UNSUPPORTED" });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getOperationalStore(target) {
|
|
16
|
+
const store = operationalContext.getStore() ?? null;
|
|
17
|
+
if (store?.active === false) {
|
|
18
|
+
const error = new Error("Operational preparation scope has already closed");
|
|
19
|
+
error.code = "E_STORAGE_TRANSACTION_EXPIRED";
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
let sameProject = !store || store.target === path.resolve(target);
|
|
23
|
+
if (!sameProject) {
|
|
24
|
+
try { sameProject = realpathSync(store.target) === realpathSync(target); }
|
|
25
|
+
catch { sameProject = false; }
|
|
26
|
+
}
|
|
27
|
+
if (!sameProject) {
|
|
28
|
+
const error = new Error("Operational store belongs to a different project");
|
|
29
|
+
error.code = "E_TASK_CONTEXT_MISMATCH";
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
return store;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readOperationalText(target, relativePath) {
|
|
36
|
+
const store = getOperationalStore(target);
|
|
37
|
+
if (!store || !store.recognizes(relativePath)) return { selected: false, text: null };
|
|
38
|
+
return { selected: true, text: store.readText(relativePath) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function operationalArtifactExists(target, relativePath) {
|
|
42
|
+
const store = getOperationalStore(target);
|
|
43
|
+
if (!store || !store.recognizes(relativePath)) return null;
|
|
44
|
+
return typeof store.artifactExists === "function" ? store.artifactExists(relativePath) : store.readText(relativePath) !== null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function listOperationalArtifactNames(target, taskId, collection) {
|
|
48
|
+
const store = getOperationalStore(target);
|
|
49
|
+
return store ? store.listArtifactNames(taskId, collection) : null;
|
|
50
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { lstat, mkdir } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { digestFile } from "./file-durability.js";
|
|
6
|
+
import { assertSafePath, writeFileAtomic } from "../core/filesystem.js";
|
|
7
|
+
import { JSON_LIMITS } from "../core/json-safety.js";
|
|
8
|
+
import { readStorageMetadataJson } from "./metadata-json.js";
|
|
9
|
+
import { backupStorageDatabase } from "./backup.js";
|
|
10
|
+
import { checkStorageIntegrity, openStorageDatabase } from "./connection.js";
|
|
11
|
+
import { iterateAttachmentReferences } from "./attachment-references.js";
|
|
12
|
+
import { publishAttachmentFile, verifyAttachmentFile } from "./attachment-files.js";
|
|
13
|
+
|
|
14
|
+
function invalid(message) { return Object.assign(new Error(message), { code: "E_STORAGE_BACKUP_INVALID" }); }
|
|
15
|
+
|
|
16
|
+
async function copyAttachment(source, destination, reference) {
|
|
17
|
+
await verifyAttachmentFile(source, reference);
|
|
18
|
+
const copied = await publishAttachmentFile(destination, createReadStream(await assertSafePath(source, reference.path)));
|
|
19
|
+
if (copied.size !== reference.size || copied.sha256 !== reference.sha256) throw invalid("Attachment changed during backup copy");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Retained database snapshot plus all canonically referenced attachment bytes. */
|
|
23
|
+
export async function backupProjectStorage(db, target, destination) {
|
|
24
|
+
if (db.isTransaction) throw invalid("Project backup requires committed storage state");
|
|
25
|
+
const directory = path.resolve(destination);
|
|
26
|
+
await assertSafePath(path.dirname(directory), path.basename(directory));
|
|
27
|
+
await mkdir(directory); // Never overwrite a retained or interrupted backup.
|
|
28
|
+
const manifestPath = await assertSafePath(directory, "backup-manifest.json");
|
|
29
|
+
const manifest = { schemaVersion: 1, kind: "SQLITE_WITH_REFERENCED_ATTACHMENTS", status: "PREPARING" };
|
|
30
|
+
const persist = () => writeFileAtomic(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
31
|
+
await persist();
|
|
32
|
+
let snapshot;
|
|
33
|
+
try {
|
|
34
|
+
const databasePath = await assertSafePath(directory, "state.sqlite");
|
|
35
|
+
await backupStorageDatabase(db, databasePath);
|
|
36
|
+
snapshot = openStorageDatabase(databasePath, { readOnly: true });
|
|
37
|
+
let references = 0;
|
|
38
|
+
async function* inventory() {
|
|
39
|
+
for (const reference of iterateAttachmentReferences(snapshot)) {
|
|
40
|
+
await copyAttachment(target, directory, reference);
|
|
41
|
+
references += 1;
|
|
42
|
+
yield `${JSON.stringify(reference)}\n`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const inventoryPath = await assertSafePath(directory, "attachments.ndjson");
|
|
46
|
+
await writeFileAtomic(inventoryPath, inventory());
|
|
47
|
+
snapshot.close(); snapshot = null;
|
|
48
|
+
Object.assign(manifest, { status: "READY", databaseSha256: await digestFile(databasePath), inventorySha256: await digestFile(inventoryPath), references });
|
|
49
|
+
await persist();
|
|
50
|
+
return { path: directory, kind: manifest.kind, attachmentsIncluded: true, references };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
manifest.status = "FAILED";
|
|
53
|
+
manifest.error = { code: error.code ?? "E_STORAGE_BACKUP_INVALID", message: error.message };
|
|
54
|
+
try { await persist(); } catch { /* PREPARING is also refused during restore. */ }
|
|
55
|
+
throw error;
|
|
56
|
+
} finally { snapshot?.close(); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function withVerifiedProjectStorageBackup(directory, callback) {
|
|
60
|
+
const root = path.resolve(directory);
|
|
61
|
+
const manifest = await readStorageMetadataJson(root, "backup-manifest.json", { limits: JSON_LIMITS });
|
|
62
|
+
if (!manifest || manifest.schemaVersion !== 1 || manifest.kind !== "SQLITE_WITH_REFERENCED_ATTACHMENTS" || manifest.status !== "READY"
|
|
63
|
+
|| !Number.isSafeInteger(manifest.references) || manifest.references < 0) throw invalid("Backup is incomplete or unsupported");
|
|
64
|
+
const filename = await assertSafePath(root, "state.sqlite");
|
|
65
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
66
|
+
const sidecar = await assertSafePath(root, `state.sqlite${suffix}`);
|
|
67
|
+
try { if (suffix === "-wal" && (await lstat(sidecar)).size !== 0) throw invalid("Backup requires unshipped WAL contents"); }
|
|
68
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
69
|
+
}
|
|
70
|
+
if (await digestFile(filename) !== manifest.databaseSha256) throw invalid("Backup database digest mismatch");
|
|
71
|
+
const db = openStorageDatabase(filename, { readOnly: true });
|
|
72
|
+
try {
|
|
73
|
+
if (!checkStorageIntegrity(db).ok) throw invalid("Backup database failed integrity");
|
|
74
|
+
let references = 0;
|
|
75
|
+
const inventory = createHash("sha256");
|
|
76
|
+
for (const reference of iterateAttachmentReferences(db)) {
|
|
77
|
+
await verifyAttachmentFile(root, reference);
|
|
78
|
+
references += 1;
|
|
79
|
+
inventory.update(`${JSON.stringify(reference)}\n`);
|
|
80
|
+
}
|
|
81
|
+
if (references !== manifest.references || inventory.digest("hex") !== manifest.inventorySha256
|
|
82
|
+
|| await digestFile(await assertSafePath(root, "attachments.ndjson")) !== manifest.inventorySha256) throw invalid("Backup inventory disagrees with canonical references");
|
|
83
|
+
return await callback({ db, root, manifest });
|
|
84
|
+
} finally { db.close(); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function verifyProjectStorageBackup(directory) {
|
|
88
|
+
return withVerifiedProjectStorageBackup(directory, ({ root, manifest }) => ({ path: root, references: manifest.references, valid: true }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Restore only into a fresh directory; never replace a running project. */
|
|
92
|
+
export async function restoreProjectStorageBackup(source, destination) {
|
|
93
|
+
return withVerifiedProjectStorageBackup(source, async ({ db, root }) => {
|
|
94
|
+
const restored = await backupProjectStorage(db, root, destination);
|
|
95
|
+
await verifyProjectStorageBackup(destination);
|
|
96
|
+
return { ...restored, restored: true, source: root };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { assertSafePath } from "../core/filesystem.js";
|
|
4
|
+
import { openStorageDatabase } from "./connection.js";
|
|
5
|
+
import { backupProjectStorage } from "./project-backup.js";
|
|
6
|
+
import { assertFreshOperationalState, restoreProjectStorageToFreshProject } from "./project-restore.js";
|
|
7
|
+
import { assertStorageNodeVersion } from "./runtime.js";
|
|
8
|
+
|
|
9
|
+
/** Publish an empty store through the same journaled path as a verified restore.
|
|
10
|
+
* Retain the seed: a PREPARING restore needs its independent source on recovery.
|
|
11
|
+
* This primitive requires writer exclusion from its caller, just like restore.
|
|
12
|
+
*/
|
|
13
|
+
export async function bootstrapProjectStorage(target, { writersQuiesced = false, packageRoot } = {}) {
|
|
14
|
+
if (writersQuiesced !== true) throw Object.assign(new Error("Exclude all writers before storage bootstrap"), { code: "E_STORAGE_MIGRATION_QUIESCENCE_REQUIRED" });
|
|
15
|
+
assertStorageNodeVersion();
|
|
16
|
+
await assertFreshOperationalState(target);
|
|
17
|
+
const root = await assertSafePath(target, `.forgeloop/storage-bootstrap/${randomUUID()}`);
|
|
18
|
+
await mkdir(root, { recursive: true });
|
|
19
|
+
const db = openStorageDatabase(await assertSafePath(root, "seed.sqlite"));
|
|
20
|
+
const source = await assertSafePath(root, "snapshot");
|
|
21
|
+
try { await backupProjectStorage(db, root, source); }
|
|
22
|
+
finally { db.close(); }
|
|
23
|
+
return restoreProjectStorageToFreshProject(source, target, { writersQuiesced, packageRoot });
|
|
24
|
+
}
|