@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,119 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const activeOwner = new AsyncLocalStorage();
|
|
6
|
+
function closed(message) { return Object.assign(new Error(message), { code: "E_STORAGE_RUNTIME_CLOSED" }); }
|
|
7
|
+
function storageChanged() { return Object.assign(new Error("Project database changed while other connection leases are active"), { code: "E_STORAGE_MIGRATION_REQUIRED" }); }
|
|
8
|
+
|
|
9
|
+
async function openOwnedConnection(entry, filename, options) {
|
|
10
|
+
if (entry.db?.isTransaction && entry.active.size > 1) throw Object.assign(new Error("Another lease left an active native transaction; overlapping access is forbidden"), { code: "E_STORAGE_TRANSACTION_LEAKED" });
|
|
11
|
+
const identity = await lstat(filename, { bigint: true });
|
|
12
|
+
const sameFile = entry.identity?.dev === identity.dev && entry.identity?.ino === identity.ino;
|
|
13
|
+
if (entry.db && (!sameFile || entry.readOnly !== options.readOnly)) {
|
|
14
|
+
if (entry.active.size > 1) throw storageChanged();
|
|
15
|
+
entry.db.close(); entry.db = null;
|
|
16
|
+
}
|
|
17
|
+
if (!entry.db) {
|
|
18
|
+
const { openStorageDatabase } = await import("./connection.js");
|
|
19
|
+
const db = openStorageDatabase(filename, options);
|
|
20
|
+
const after = await lstat(filename, { bigint: true });
|
|
21
|
+
if (after.dev !== identity.dev || after.ino !== identity.ino) { db.close(); throw closed("Project database changed while opening its connection"); }
|
|
22
|
+
entry.db = db; entry.identity = after; entry.readOnly = options.readOnly;
|
|
23
|
+
}
|
|
24
|
+
const { assertStorageMetadataCurrent, assertProjectDatabaseAdmission } = await import("./connection.js");
|
|
25
|
+
assertProjectDatabaseAdmission(entry.db, options.projectAdmission);
|
|
26
|
+
assertStorageMetadataCurrent(entry.db);
|
|
27
|
+
return entry.db;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function connectionLease(entry, scope) {
|
|
31
|
+
return {
|
|
32
|
+
assertUnselected() {
|
|
33
|
+
if (entry.db) throw Object.assign(new Error("Previously selected project storage has lost its database; filesystem fallback is forbidden"), { code: "E_STORAGE_MIGRATION_REQUIRED" });
|
|
34
|
+
},
|
|
35
|
+
open(filename, options) {
|
|
36
|
+
// Opening/replacing a handle includes filesystem awaits. Serialize only
|
|
37
|
+
// this short admission work, never the external command callback.
|
|
38
|
+
const opening = entry.openTail.then(() => openOwnedConnection(entry, filename, options));
|
|
39
|
+
entry.openTail = opening.then(() => undefined, () => undefined);
|
|
40
|
+
return opening.then(db => { scope.opened = true; return db; });
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function admitLease(entry, readOnly) {
|
|
46
|
+
let release;
|
|
47
|
+
const lifetime = new Promise(resolve => { release = resolve; });
|
|
48
|
+
const admission = entry.tail.then(async () => {
|
|
49
|
+
if (entry.mode !== readOnly) await Promise.all([...entry.active]);
|
|
50
|
+
entry.mode = readOnly;
|
|
51
|
+
entry.active.add(lifetime);
|
|
52
|
+
});
|
|
53
|
+
entry.tail = admission.then(() => undefined, () => undefined);
|
|
54
|
+
await admission;
|
|
55
|
+
return () => { entry.active.delete(lifetime); release(); };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function executeLease(entry, owner, callback, readOnly) {
|
|
59
|
+
const release = await admitLease(entry, readOnly);
|
|
60
|
+
const scope = { owner, active: true, opened: false };
|
|
61
|
+
try {
|
|
62
|
+
return await activeOwner.run(scope, () => callback(connectionLease(entry, scope)));
|
|
63
|
+
} finally {
|
|
64
|
+
scope.active = false;
|
|
65
|
+
try {
|
|
66
|
+
if (scope.opened && entry.db?.isTransaction) {
|
|
67
|
+
entry.db.close(); entry.db = null;
|
|
68
|
+
throw Object.assign(new Error("Command left an active native storage transaction; connection was closed"), { code: "E_STORAGE_TRANSACTION_LEAKED" });
|
|
69
|
+
}
|
|
70
|
+
} finally { release(); }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One connection per project; same-mode leases share it without external serialization. */
|
|
75
|
+
export function createStorageConnectionOwner() {
|
|
76
|
+
const entries = new Map();
|
|
77
|
+
const pending = new Set();
|
|
78
|
+
let closing = false;
|
|
79
|
+
let closePromise;
|
|
80
|
+
const owner = {
|
|
81
|
+
isActive() { return activeOwner.getStore()?.owner === owner && activeOwner.getStore()?.active === true; },
|
|
82
|
+
run(target, callback, { readOnly = false } = {}) {
|
|
83
|
+
if (closing) return Promise.reject(closed("Persistent storage runtime is closing or closed"));
|
|
84
|
+
if (owner.isActive()) return Promise.reject(closed("Nested connection ownership must reuse the active operational scope"));
|
|
85
|
+
const request = (async () => {
|
|
86
|
+
let key;
|
|
87
|
+
try { key = await realpath(path.resolve(target)); }
|
|
88
|
+
catch (error) { if (error.code !== "ENOENT") throw error; key = path.resolve(target); }
|
|
89
|
+
let entry = entries.get(key);
|
|
90
|
+
if (!entry) {
|
|
91
|
+
entry = { tail: Promise.resolve(), openTail: Promise.resolve(), active: new Set(), mode: null, db: null, identity: null };
|
|
92
|
+
entries.set(key, entry);
|
|
93
|
+
}
|
|
94
|
+
return executeLease(entry, owner, callback, readOnly);
|
|
95
|
+
})();
|
|
96
|
+
pending.add(request);
|
|
97
|
+
const finished = () => { pending.delete(request); };
|
|
98
|
+
request.then(finished, finished);
|
|
99
|
+
return request;
|
|
100
|
+
},
|
|
101
|
+
close() {
|
|
102
|
+
if (owner.isActive()) return Promise.reject(closed("Close the runtime outside an active command"));
|
|
103
|
+
if (closePromise) return closePromise;
|
|
104
|
+
closing = true;
|
|
105
|
+
closePromise = (async () => {
|
|
106
|
+
await Promise.all([...pending].map(request => request.catch(() => undefined)));
|
|
107
|
+
let failure;
|
|
108
|
+
for (const entry of entries.values()) {
|
|
109
|
+
try { entry.db?.close(); } catch (error) { failure ??= error; }
|
|
110
|
+
entry.db = null;
|
|
111
|
+
}
|
|
112
|
+
entries.clear();
|
|
113
|
+
if (failure) throw failure;
|
|
114
|
+
})();
|
|
115
|
+
return closePromise;
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
return owner;
|
|
119
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { artifactByteDigest } from "./artifact-bytes.js";
|
|
2
|
+
import { assertStorageTopology } from "./topology.js";
|
|
3
|
+
import { validateAttachmentReference } from "./attachment-binding.js";
|
|
4
|
+
import { canonicalFingerprint } from "../core/artifacts.js";
|
|
5
|
+
import { eventHash, validateKnownEventDetails } from "../core/events.js";
|
|
6
|
+
import { loadStorageDriver } from "./runtime.js";
|
|
7
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { isPathWithin } from "../core/filesystem.js";
|
|
10
|
+
|
|
11
|
+
import { PROTOCOL_VERSION } from "../core/protocol.js";
|
|
12
|
+
import { OPTIONAL_STORAGE_INDEXES, SCHEMA_MIGRATIONS, STORAGE_FORMAT, STORAGE_FORMAT_VERSION, STORAGE_SCHEMA_VERSION } from "./schema.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Canonical on-disk location of the authoritative operational store for one
|
|
16
|
+
* project state root. One database per state root keeps task/claim/event
|
|
17
|
+
* mutations inside a single transaction without cross-database coordination.
|
|
18
|
+
*/
|
|
19
|
+
export { STORAGE_RELATIVE_PATH } from "./schema.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Durability and concurrency defaults.
|
|
23
|
+
*
|
|
24
|
+
* `synchronous=FULL` matches the durability the filesystem transaction
|
|
25
|
+
* implementation provided via fsync-before-rename, so migration does not
|
|
26
|
+
* silently trade crash safety for speed. `busy_timeout` mirrors the 5s
|
|
27
|
+
* transaction wait the previous write-lock loop allowed.
|
|
28
|
+
*/
|
|
29
|
+
export const BUSY_TIMEOUT_MS = 5_000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pragma settings that must be confirmed after they are applied.
|
|
33
|
+
*
|
|
34
|
+
* `column` differs from `pragma` for busy_timeout: SQLite reports that value
|
|
35
|
+
* under the key `timeout`, so asserting on the pragma name would silently read
|
|
36
|
+
* `undefined` and reject every store.
|
|
37
|
+
*/
|
|
38
|
+
function pragmaAssertions(busyTimeoutMs) {
|
|
39
|
+
return [
|
|
40
|
+
{ pragma: "journal_mode", column: "journal_mode", expected: "wal" },
|
|
41
|
+
{ pragma: "foreign_keys", column: "foreign_keys", expected: 1 },
|
|
42
|
+
{ pragma: "synchronous", column: "synchronous", expected: 2 },
|
|
43
|
+
{ pragma: "busy_timeout", column: "timeout", expected: busyTimeoutMs },
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Stable error codes surfaced to callers and mapped by the CLI layer. */
|
|
48
|
+
export const STORAGE_ERROR_CODES = Object.freeze({
|
|
49
|
+
CORRUPT: "E_STORAGE_CORRUPT",
|
|
50
|
+
READ_ONLY: "E_STORAGE_READ_ONLY",
|
|
51
|
+
BUSY: "E_STORAGE_BUSY",
|
|
52
|
+
FULL: "E_STORAGE_FULL",
|
|
53
|
+
INTERRUPTED: "E_STORAGE_INTERRUPTED",
|
|
54
|
+
UNSUPPORTED_VERSION: "E_STORAGE_VERSION_UNSUPPORTED",
|
|
55
|
+
UNSUPPORTED_RUNTIME: "E_STORAGE_UNSUPPORTED_RUNTIME",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* SQLite primary result codes that map to an explicitly recoverable condition.
|
|
60
|
+
*
|
|
61
|
+
* `node:sqlite` reports `errcode` as the numeric SQLite result code, so the
|
|
62
|
+
* mapping is keyed by number. A previous implementation keyed on symbolic
|
|
63
|
+
* strings and therefore never matched, letting a locked database surface as an
|
|
64
|
+
* opaque `ERR_SQLITE_ERROR` instead of a documented busy condition.
|
|
65
|
+
*/
|
|
66
|
+
const SQLITE_RESULT_CODE_MAP = Object.freeze({
|
|
67
|
+
5: STORAGE_ERROR_CODES.BUSY, // SQLITE_BUSY
|
|
68
|
+
6: STORAGE_ERROR_CODES.BUSY, // SQLITE_LOCKED
|
|
69
|
+
8: STORAGE_ERROR_CODES.READ_ONLY, // SQLITE_READONLY
|
|
70
|
+
11: STORAGE_ERROR_CODES.CORRUPT, // SQLITE_CORRUPT
|
|
71
|
+
26: STORAGE_ERROR_CODES.CORRUPT, // SQLITE_NOTADB (damaged database header)
|
|
72
|
+
13: STORAGE_ERROR_CODES.FULL, // SQLITE_FULL
|
|
73
|
+
14: STORAGE_ERROR_CODES.INTERRUPTED, // SQLITE_CANTOPEN
|
|
74
|
+
10: STORAGE_ERROR_CODES.INTERRUPTED, // SQLITE_IOERR
|
|
75
|
+
9: STORAGE_ERROR_CODES.INTERRUPTED, // SQLITE_INTERRUPT
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/** Extended result codes that carry the same primary code in their low bits. */
|
|
79
|
+
function primaryResultCode(errcode) {
|
|
80
|
+
if (typeof errcode === "number") return errcode & 0xff;
|
|
81
|
+
if (typeof errcode === "string" && /^\d+$/.test(errcode)) return Number(errcode) & 0xff;
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Detect whether this runtime can host the store at all.
|
|
87
|
+
*/
|
|
88
|
+
export function assertStorageRuntime() {
|
|
89
|
+
return loadStorageDriver();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Translate a raw `node:sqlite` failure into a stable, explicitly recoverable
|
|
94
|
+
* storage error. Anything unrecognized is rethrown untouched so real defects are
|
|
95
|
+
* never masked as environment conditions.
|
|
96
|
+
*/
|
|
97
|
+
export function translateStorageError(error) {
|
|
98
|
+
const primary = primaryResultCode(error?.errcode);
|
|
99
|
+
const mapped = primary === null ? null : SQLITE_RESULT_CODE_MAP[primary];
|
|
100
|
+
if (!mapped) throw error;
|
|
101
|
+
const translated = new Error(
|
|
102
|
+
`SQLite storage operation failed (SQLITE_${primary}, ${error.errstr ?? error.message}): ${error.message}`,
|
|
103
|
+
);
|
|
104
|
+
translated.code = mapped;
|
|
105
|
+
translated.sqliteResultCode = primary;
|
|
106
|
+
translated.cause = error;
|
|
107
|
+
throw translated;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Apply the connection defaults. Extension loading is never enabled and all
|
|
111
|
+
* callers bind SQL parameters, so user-authored text can never be interpreted
|
|
112
|
+
* as schema or executable SQL.
|
|
113
|
+
*/
|
|
114
|
+
function applyConnectionSettings(db, busyTimeoutMs) {
|
|
115
|
+
db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
|
|
116
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
117
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
118
|
+
db.exec("PRAGMA synchronous = FULL");
|
|
119
|
+
db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Confirm the effective settings rather than trusting that the statements
|
|
124
|
+
* succeeded. A filesystem that silently refuses WAL must fail loudly here
|
|
125
|
+
* instead of degrading durability invisibly.
|
|
126
|
+
*/
|
|
127
|
+
function assertConnectionSettings(db, busyTimeoutMs) {
|
|
128
|
+
for (const assertion of pragmaAssertions(busyTimeoutMs)) {
|
|
129
|
+
const row = db.prepare(`PRAGMA ${assertion.pragma}`).get();
|
|
130
|
+
const actual = row?.[assertion.column];
|
|
131
|
+
const normalized = typeof actual === "string" ? actual.toLowerCase() : actual;
|
|
132
|
+
if (normalized !== assertion.expected) {
|
|
133
|
+
const error = new Error(
|
|
134
|
+
`SQLite refused required pragma ${assertion.pragma} (expected ${assertion.expected}, got ${actual})`,
|
|
135
|
+
);
|
|
136
|
+
error.code = STORAGE_ERROR_CODES.UNSUPPORTED_RUNTIME;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create every table that is not yet present, then stamp the schema version.
|
|
144
|
+
*
|
|
145
|
+
* The whole migration runs inside one immediate transaction so that two
|
|
146
|
+
* processes opening the same fresh database cannot interleave their
|
|
147
|
+
* check-then-create sequence. Without that, a concurrent opener could observe
|
|
148
|
+
* a partially created schema and fail with a busy or "table already exists"
|
|
149
|
+
* error before it ever reached application code.
|
|
150
|
+
*/
|
|
151
|
+
function assertCompatibleMetadata(db, { readOnly = false } = {}) {
|
|
152
|
+
const meta = db.prepare("SELECT * FROM storage_meta WHERE id = 1").get();
|
|
153
|
+
if (!meta || !Number.isInteger(meta.schema_version) || meta.schema_version < 1
|
|
154
|
+
|| meta.schema_version > STORAGE_SCHEMA_VERSION || meta.storage_version !== STORAGE_FORMAT_VERSION
|
|
155
|
+
|| meta.storage_format !== STORAGE_FORMAT || meta.protocol_version !== PROTOCOL_VERSION) {
|
|
156
|
+
const error = new Error("Storage metadata is missing or incompatible with this runtime");
|
|
157
|
+
error.code = STORAGE_ERROR_CODES.UNSUPPORTED_VERSION;
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
if (readOnly && meta.schema_version !== STORAGE_SCHEMA_VERSION) {
|
|
161
|
+
const error = new Error("Storage schema requires an explicit writable upgrade before read-only access");
|
|
162
|
+
error.code = "E_STORAGE_MIGRATION_REQUIRED";
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function assertStorageMetadataCurrent(db) {
|
|
168
|
+
assertCompatibleMetadata(db, { readOnly: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function applyMigrations(db, { now = new Date().toISOString() } = {}) {
|
|
172
|
+
const hasMetadata = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'storage_meta'").get();
|
|
173
|
+
if (hasMetadata) {
|
|
174
|
+
assertCompatibleMetadata(db);
|
|
175
|
+
if (db.prepare("SELECT schema_version FROM storage_meta WHERE id = 1").get().schema_version === STORAGE_SCHEMA_VERSION) return;
|
|
176
|
+
}
|
|
177
|
+
db.exec("BEGIN IMMEDIATE");
|
|
178
|
+
try {
|
|
179
|
+
const existing = new Set(
|
|
180
|
+
db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((row) => row.name),
|
|
181
|
+
);
|
|
182
|
+
if (existing.has("storage_meta")) assertCompatibleMetadata(db);
|
|
183
|
+
for (const migration of SCHEMA_MIGRATIONS) {
|
|
184
|
+
if (existing.has("storage_meta")) {
|
|
185
|
+
const row = db.prepare("SELECT schema_version FROM storage_meta WHERE id = 1").get();
|
|
186
|
+
if (row && Number(row.schema_version) >= migration.version) continue;
|
|
187
|
+
}
|
|
188
|
+
for (const statement of migration.statements) {
|
|
189
|
+
if (statement.startsWith("CREATE TABLE ")) {
|
|
190
|
+
const table = statement.slice("CREATE TABLE ".length).split(/[\s(]/)[0];
|
|
191
|
+
if (existing.has(table)) continue;
|
|
192
|
+
existing.add(table);
|
|
193
|
+
}
|
|
194
|
+
db.exec(statement);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
db.prepare(
|
|
198
|
+
`INSERT INTO storage_meta (id, storage_version, schema_version, protocol_version, storage_format, created_at, updated_at)
|
|
199
|
+
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
200
|
+
ON CONFLICT (id) DO UPDATE SET schema_version = excluded.schema_version, updated_at = excluded.updated_at`,
|
|
201
|
+
).run(STORAGE_FORMAT_VERSION, STORAGE_SCHEMA_VERSION, PROTOCOL_VERSION ?? null, STORAGE_FORMAT, now, now);
|
|
202
|
+
db.exec("COMMIT");
|
|
203
|
+
} catch (error) {
|
|
204
|
+
try { db.exec("ROLLBACK"); } catch { /* preserve the migration failure */ }
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function ensureOptionalIndexes(db) {
|
|
210
|
+
const exists = db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'index' AND name = ?");
|
|
211
|
+
for (const index of OPTIONAL_STORAGE_INDEXES) {
|
|
212
|
+
if (!exists.get(index.name)) db.exec(index.statement);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Check SQLite's opened filename before configuration or reuse of a project handle. */
|
|
217
|
+
export function assertProjectDatabaseAdmission(db, admission) {
|
|
218
|
+
if (!admission) return;
|
|
219
|
+
const expected = path.join(admission.root, ".forgeloop/state.sqlite");
|
|
220
|
+
const changed = () => Object.assign(new Error("Project database path changed while opening or reusing its connection"), { code: "E_STORAGE_MIGRATION_REQUIRED" });
|
|
221
|
+
try {
|
|
222
|
+
const root = lstatSync(admission.target, { bigint: true });
|
|
223
|
+
if (!root.isDirectory() || root.isSymbolicLink()) throw changed();
|
|
224
|
+
if (admission.rootIdentity && ["dev", "ino", "mtimeNs", "ctimeNs"].some(key => root[key] !== admission.rootIdentity[key])) throw changed();
|
|
225
|
+
const opened = realpathSync.native(db.location());
|
|
226
|
+
const resolvedExpected = realpathSync.native(expected);
|
|
227
|
+
const current = lstatSync(expected, { bigint: true });
|
|
228
|
+
if (!isPathWithin(resolvedExpected, opened) || !isPathWithin(opened, resolvedExpected)
|
|
229
|
+
|| !current.isFile() || current.isSymbolicLink()
|
|
230
|
+
|| current.dev !== admission.dev || current.ino !== admission.ino) throw changed();
|
|
231
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
232
|
+
try {
|
|
233
|
+
const sidecar = lstatSync(`${expected}${suffix}`);
|
|
234
|
+
if (!sidecar.isFile() || sidecar.isSymbolicLink()) throw changed();
|
|
235
|
+
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code)) throw changed();
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Open the operational store, apply durability defaults, and ensure the schema
|
|
245
|
+
* is current. The caller owns the returned handle and must `close()` it.
|
|
246
|
+
*/
|
|
247
|
+
export function openStorageDatabase(databasePath, {
|
|
248
|
+
readOnly = false,
|
|
249
|
+
allowSchemaUpgrade = true,
|
|
250
|
+
allowOptionalIndexCreation = allowSchemaUpgrade,
|
|
251
|
+
now = new Date().toISOString(),
|
|
252
|
+
busyTimeoutMs = BUSY_TIMEOUT_MS,
|
|
253
|
+
projectAdmission = null,
|
|
254
|
+
} = {}) {
|
|
255
|
+
const { DatabaseSync } = assertStorageRuntime();
|
|
256
|
+
if (!Number.isInteger(busyTimeoutMs) || busyTimeoutMs < 0 || busyTimeoutMs > 2_147_483_647) {
|
|
257
|
+
const error = new Error("busyTimeoutMs must be a non-negative SQLite timeout integer");
|
|
258
|
+
error.code = "E_STORAGE_CONFIGURATION_INVALID";
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
assertStorageTopology(databasePath);
|
|
262
|
+
let db;
|
|
263
|
+
try {
|
|
264
|
+
db = new DatabaseSync(databasePath, { readOnly, open: true, enableForeignKeyConstraints: true });
|
|
265
|
+
assertProjectDatabaseAdmission(db, projectAdmission);
|
|
266
|
+
applyConnectionSettings(db, busyTimeoutMs);
|
|
267
|
+
assertConnectionSettings(db, busyTimeoutMs);
|
|
268
|
+
if (!readOnly && allowSchemaUpgrade) applyMigrations(db, { now });
|
|
269
|
+
else assertCompatibleMetadata(db, { readOnly: true });
|
|
270
|
+
// Optional indexes preserve the marker-bound schema and exact fallback query.
|
|
271
|
+
// Inspect first: opening an indexed store needs no DDL writer reservation.
|
|
272
|
+
if (!readOnly && allowOptionalIndexCreation) ensureOptionalIndexes(db);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (db) {
|
|
275
|
+
try { db.close(); } catch { /* preserve the original failure */ }
|
|
276
|
+
}
|
|
277
|
+
if (typeof error?.code === "string" && error.code.startsWith("E_STORAGE_")) throw error;
|
|
278
|
+
return translateStorageError(error);
|
|
279
|
+
}
|
|
280
|
+
return db;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Read the store metadata singleton, or `null` for an uninitialized database. */
|
|
284
|
+
export function readStorageMeta(db) {
|
|
285
|
+
const table = db
|
|
286
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'storage_meta'")
|
|
287
|
+
.get();
|
|
288
|
+
if (!table) return null;
|
|
289
|
+
return db.prepare("SELECT * FROM storage_meta WHERE id = 1").get() ?? null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function validateStoredEvent(row, sequence, previousHash) {
|
|
293
|
+
const event = JSON.parse(row.event_json);
|
|
294
|
+
if (event.taskId !== row.task_id || event.seq !== row.seq || event.event !== row.event_type
|
|
295
|
+
|| event.at !== row.at || event.hash !== row.hash || event.previousHash !== row.previous_hash
|
|
296
|
+
|| event.seq !== sequence || event.previousHash !== previousHash || event.hash !== eventHash(event)) {
|
|
297
|
+
throw Object.assign(new Error("Stored ledger identity, ordering or hash mismatch"), { code: "E_LEDGER_HASH_INVALID" });
|
|
298
|
+
}
|
|
299
|
+
validateKnownEventDetails(event);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function collectNativeFindings(statement, project = row => row) {
|
|
303
|
+
const findings = [];
|
|
304
|
+
let total = 0;
|
|
305
|
+
for (const row of statement.iterate()) {
|
|
306
|
+
total += 1;
|
|
307
|
+
if (findings.length < 100) findings.push(project(row));
|
|
308
|
+
}
|
|
309
|
+
return { findings, total };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function nativeIntegrityFindings(db) {
|
|
313
|
+
const integrity = collectNativeFindings(db.prepare("PRAGMA integrity_check"), row => row.integrity_check);
|
|
314
|
+
const foreignKeys = collectNativeFindings(db.prepare("PRAGMA foreign_key_check"));
|
|
315
|
+
const truncatedFindings = {};
|
|
316
|
+
for (const [name, result] of Object.entries({ integrity, foreignKeys })) {
|
|
317
|
+
if (result.total > result.findings.length) truncatedFindings[name] = { total: result.total, reported: result.findings.length };
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
integrity: integrity.findings,
|
|
321
|
+
foreignKeys: foreignKeys.findings,
|
|
322
|
+
...(Object.keys(truncatedFindings).length ? { truncatedFindings } : {}),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Run the integrity and foreign-key checks the plan requires before a cutover
|
|
328
|
+
* is published. Scans all rows but retains at most 100 native findings per
|
|
329
|
+
* check, reporting truncation instead of silently dropping diagnostics.
|
|
330
|
+
*/
|
|
331
|
+
export function checkStorageIntegrity(db) {
|
|
332
|
+
const native = nativeIntegrityFindings(db);
|
|
333
|
+
const { integrity, foreignKeys } = native;
|
|
334
|
+
const artifactErrors = [];
|
|
335
|
+
const eventErrors = [];
|
|
336
|
+
const attachmentErrors = [];
|
|
337
|
+
let taskId = null;
|
|
338
|
+
let sequence = 0;
|
|
339
|
+
let previousHash = null;
|
|
340
|
+
for (const row of db.prepare("SELECT * FROM events ORDER BY task_id, seq").iterate()) {
|
|
341
|
+
if (row.task_id !== taskId) { taskId = row.task_id; sequence = 0; previousHash = null; }
|
|
342
|
+
sequence += 1;
|
|
343
|
+
try {
|
|
344
|
+
validateStoredEvent(row, sequence, previousHash);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
// Findings are bounded even when every event of a large ledger is bad.
|
|
347
|
+
if (eventErrors.length < 100) eventErrors.push({ taskId, sequence: row.seq, code: error.code ?? "E_EVENT_INVALID" });
|
|
348
|
+
}
|
|
349
|
+
previousHash = row.hash;
|
|
350
|
+
}
|
|
351
|
+
for (const row of db.prepare("SELECT task_id, kind, artifact_id, payload_json, fingerprint, source_json, byte_digest FROM task_artifacts").iterate()) {
|
|
352
|
+
try {
|
|
353
|
+
if (row.fingerprint !== canonicalFingerprint(JSON.parse(row.payload_json))) throw Object.assign(new Error("Artifact fingerprint mismatch"), { code: "E_STORAGE_ARTIFACT_INVALID" });
|
|
354
|
+
if (row.source_json === null && row.byte_digest === null) continue;
|
|
355
|
+
artifactByteDigest({ sourceText: row.source_json, byteDigest: row.byte_digest, payload: JSON.parse(row.payload_json) });
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (artifactErrors.length < 100) artifactErrors.push({ taskId: row.task_id, kind: row.kind, artifactId: row.artifact_id, code: error.code ?? "E_STORAGE_ARTIFACT_INVALID" });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
for (const row of db.prepare("SELECT * FROM attachment_references ORDER BY task_id, reference_id").iterate()) {
|
|
361
|
+
try {
|
|
362
|
+
validateAttachmentReference({ taskId: row.task_id, referenceId: row.reference_id, path: row.path, size: row.size, sha256: row.sha256 });
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (attachmentErrors.length < 100) attachmentErrors.push({ taskId: row.task_id, referenceId: row.reference_id, code: error.code ?? "E_STORAGE_ATTACHMENT_INVALID" });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
artifactErrors,
|
|
369
|
+
eventErrors,
|
|
370
|
+
attachmentErrors,
|
|
371
|
+
ok: attachmentErrors.length === 0 && eventErrors.length === 0 && artifactErrors.length === 0 && integrity.length === 1 && integrity[0] === "ok" && foreignKeys.length === 0,
|
|
372
|
+
integrity,
|
|
373
|
+
foreignKeys,
|
|
374
|
+
...native,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { assertSafePath, fileExists } from "../core/filesystem.js";
|
|
3
|
+
import { LEGACY_TASK_ARTIFACT_PATHS } from "../core/task-paths.js";
|
|
4
|
+
import { getOperationalStore } from "./operational-context.js";
|
|
5
|
+
|
|
6
|
+
/** Select existing native authority only; never bootstrap or convert legacy data. */
|
|
7
|
+
export async function needsExistingProjectScope(target) {
|
|
8
|
+
if (getOperationalStore(target)) return false;
|
|
9
|
+
for (const relative of [".forgeloop/state.sqlite", ".forgeloop/storage-version.json"]) {
|
|
10
|
+
if (await fileExists(await assertSafePath(target, relative))) return true;
|
|
11
|
+
}
|
|
12
|
+
for (const relative of [".forgeloop/state.sqlite-wal", ".forgeloop/state.sqlite-shm"]) {
|
|
13
|
+
if (await fileExists(await assertSafePath(target, relative))) {
|
|
14
|
+
throw Object.assign(new Error("Retained SQLite sidecars require explicit storage reconciliation; filesystem fallback is forbidden"), { code: "E_STORAGE_MIGRATION_REQUIRED" });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function withExistingProjectScope(target, callback, { readOnly = false } = {}) {
|
|
21
|
+
const { withProjectStorage } = await import("./project-boundary.js");
|
|
22
|
+
return withProjectStorage(target, callback, { readOnly, existingOnly: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Operational namespaces require canonical SQLite even before first creation. */
|
|
26
|
+
export function isOperationalArtifactPath(relativePath) {
|
|
27
|
+
const normalized = path.posix.normalize(relativePath.replaceAll("\\", "/"))
|
|
28
|
+
.split("/").map(segment => segment.replace(/[. ]+$/, "").split(":")[0].toLowerCase()).join("/");
|
|
29
|
+
return [...Object.values(LEGACY_TASK_ARTIFACT_PATHS), ".forgeloop/task-state", ".forgeloop/sessions", ".forgeloop/.txn"]
|
|
30
|
+
.some(root => normalized === root || normalized.startsWith(`${root}/`));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A native project cannot acquire new writable singleton operational aliases. */
|
|
34
|
+
export function assertNativeWritePath(target, relativePath) {
|
|
35
|
+
const store = getOperationalStore(target);
|
|
36
|
+
const normalized = path.posix.normalize(relativePath.replaceAll("\\", "/"))
|
|
37
|
+
.split("/").map(segment => segment.replace(/[. ]+$/, "").split(":")[0].toLowerCase()).join("/");
|
|
38
|
+
const reservedRoots = [...Object.values(LEGACY_TASK_ARTIFACT_PATHS), ".forgeloop/task-state", ".forgeloop/sessions", ".forgeloop/.txn"];
|
|
39
|
+
const databasePath = normalized === ".forgeloop/state.sqlite" || normalized.startsWith(".forgeloop/state.sqlite-")
|
|
40
|
+
|| normalized === ".forgeloop/storage-version.json";
|
|
41
|
+
if (!databasePath && (!store || store.recognizes(relativePath))) return;
|
|
42
|
+
if (databasePath || reservedRoots.some(root => normalized === root || normalized.startsWith(`${root}/`))) {
|
|
43
|
+
throw Object.assign(new Error("SQLite operational writes require a canonical task-scoped path; legacy singleton persistence is retired"), { code: "E_STORAGE_OPERATION_UNSUPPORTED", artifacts: [relativePath] });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { realpathWithTransientWindowsRetry } from "../core/filesystem.js";
|
|
4
|
+
import { isOperationalArtifactPath } from "./existing-project-scope.js";
|
|
5
|
+
|
|
6
|
+
const NATIVE_EVIDENCE = ["state.sqlite", "state.sqlite-wal", "state.sqlite-shm", "storage-version.json"];
|
|
7
|
+
|
|
8
|
+
async function physicalDestination(destination) {
|
|
9
|
+
let ancestor = path.resolve(destination);
|
|
10
|
+
const missing = [];
|
|
11
|
+
while (true) {
|
|
12
|
+
try { return path.join(await realpathWithTransientWindowsRetry(ancestor), ...missing); }
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code !== "ENOENT") throw error;
|
|
15
|
+
missing.unshift(path.basename(ancestor));
|
|
16
|
+
const parent = path.dirname(ancestor);
|
|
17
|
+
if (parent === ancestor) throw error;
|
|
18
|
+
ancestor = parent;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function exists(filename) {
|
|
24
|
+
try { await lstat(filename); return true; }
|
|
25
|
+
catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Portable output cannot recreate operational files alongside native authority. */
|
|
29
|
+
export async function assertPortableExportDestination(destination) {
|
|
30
|
+
const physical = await physicalDestination(destination);
|
|
31
|
+
let ancestor = physical;
|
|
32
|
+
while (true) {
|
|
33
|
+
const relative = path.relative(ancestor, physical).split(path.sep).join("/");
|
|
34
|
+
if (relative === "" || isOperationalArtifactPath(relative)) {
|
|
35
|
+
const evidence = await Promise.all(NATIVE_EVIDENCE.map(name => exists(path.join(ancestor, ".forgeloop", name))));
|
|
36
|
+
if (evidence.some(Boolean)) {
|
|
37
|
+
throw Object.assign(new Error("Portable export requires a separate destination outside native operational namespaces"), { code: "E_STORAGE_EXPORT_DESTINATION" });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const parent = path.dirname(ancestor);
|
|
41
|
+
if (parent === ancestor) return;
|
|
42
|
+
ancestor = parent;
|
|
43
|
+
}
|
|
44
|
+
}
|