@cassiomc1/forgeloop 1.14.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/CONTRIBUTING.md +11 -7
- package/DOCS_INDEX.md +2 -1
- 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 +8 -4
- 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/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/GUIDE_ROUTER.md +6 -0
- package/LICENSE +133 -21
- package/LOOP_SYSTEM_DESIGN.md +10 -10
- package/README.md +28 -39
- package/THIRD_PARTY_NOTICES.md +3 -4
- package/completions/_forgeloop +11 -3
- package/completions/forgeloop.bash +19 -3
- package/completions/forgeloop.fish +61 -1
- package/docs/AGENT_PROTOCOL_SUMMARY.md +50 -40
- package/docs/ARTIFACT_REFERENCE.md +50 -37
- package/docs/CLI_REFERENCE.md +147 -3
- package/docs/DOCUMENTATION_GUIDE.md +6 -4
- package/docs/GETTING_STARTED.md +3 -3
- package/docs/PACKAGE_CONTENTS.md +14 -13
- package/docs/RELEASE_CHECKLIST.md +81 -10
- package/docs/SELF_HOSTED_TESTS.md +121 -0
- package/docs/documentation-manifest.json +25 -12
- package/package.json +15 -10
- package/schemas/task-bundle.schema.json +7 -2
- package/scripts/generate-agent-protocol-summary.mjs +2 -2
- package/scripts/run-mcp-clean-tests.mjs +30 -0
- package/src/adapters/agent-browser/provider.js +14 -11
- package/src/adapters/emulated-services/provider.js +11 -4
- package/src/adapters/temporary-paths.js +18 -0
- package/src/cli.js +146 -210
- 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 +24 -8
- package/src/commands/complete.js +19 -3
- package/src/commands/contract-create.js +3 -1
- package/src/commands/doctor.js +36 -13
- package/src/commands/gate-record.js +20 -31
- package/src/commands/gate-revalidate.js +11 -13
- 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 +9 -1
- 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/semantic-plan.js +26 -2
- 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 +1 -1
- package/src/commands/task-list.js +8 -43
- package/src/commands/task-migrate.js +3 -3
- package/src/commands/task-recover.js +10 -2
- package/src/commands/task-scope.js +18 -2
- package/src/commands/task-show.js +16 -2
- package/src/commands/validate-protocol.js +29 -14
- 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 +59 -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.js +6 -1
- package/src/core/bundles.js +160 -66
- package/src/core/checkpoint-revalidation.js +12 -10
- package/src/core/cli-command-definitions.js +109 -7
- package/src/core/code-manifest.js +9 -3
- package/src/core/command-executors.js +182 -126
- package/src/core/command-input.js +21 -1
- package/src/core/completion-artifacts.js +71 -29
- package/src/core/completion-ownership.js +19 -8
- package/src/core/completion-recovery-rebind.js +11 -1
- package/src/core/completion.js +60 -9
- package/src/core/conformance.js +2 -1
- 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 +42 -41
- package/src/core/contract-revision.js +8 -9
- package/src/core/decision/artifact.js +2 -1
- package/src/core/decision/events.js +9 -6
- package/src/core/decision/resolver.js +19 -11
- package/src/core/decision/service.js +63 -38
- package/src/core/decision/task-bindings.js +6 -1
- 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/events.js +316 -97
- package/src/core/execution-prerequisites.js +22 -13
- 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 +6 -5
- 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 +10 -0
- package/src/core/integration-resources.js +41 -34
- package/src/core/ledger-event-collection.js +189 -0
- package/src/core/ledger-relations.js +23 -0
- package/src/core/native-storage.js +12 -0
- package/src/core/next-action-continuity.js +31 -9
- package/src/core/next-action-phases.js +1 -1
- package/src/core/next-action-review-phase.js +2 -2
- package/src/core/next-action-verification-phase.js +2 -1
- package/src/core/next-action.js +21 -2
- package/src/core/phase.js +138 -93
- package/src/core/policy-engine.js +12 -13
- package/src/core/preflight-consistency.js +72 -59
- package/src/core/preflight-loaders.js +19 -12
- 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 +3 -0
- package/src/core/protocol-migration.js +7 -6
- package/src/core/reconcile-closure.js +162 -24
- package/src/core/recovery-history.js +66 -47
- package/src/core/reflection.js +72 -77
- package/src/core/responsibility.js +25 -13
- package/src/core/route-artifact.js +13 -3
- package/src/core/runtime-context.js +5 -0
- package/src/core/schema-validation.js +8 -3
- 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/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 +52 -18
- package/src/core/task-command.js +21 -1
- package/src/core/task-conflict-inspection.js +112 -107
- 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-snapshot.js +51 -8
- package/src/core/task-storage-migration.js +18 -0
- package/src/core/templates.js +20 -8
- package/src/core/test-intelligence/prune.js +5 -1
- package/src/core/trace.js +154 -99
- package/src/core/trajectory-evaluation.js +15 -3
- package/src/core/trajectory-metrics.js +47 -32
- package/src/core/transaction.js +16 -288
- package/src/core/verification-scope.js +20 -12
- package/src/core/work-state.js +100 -59
- 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,79 @@
|
|
|
1
|
+
import { getStorageConnectionOwner } from "../core/storage-runtime-registry.js";
|
|
2
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
+
import { assertSafePath } from "../core/filesystem.js";
|
|
4
|
+
import { getOperationalStore } from "./operational-context.js";
|
|
5
|
+
import { assertStorageMaintenanceInactive } from "./maintenance.js";
|
|
6
|
+
import { readStorageVersionMarker } from "./storage-marker.js";
|
|
7
|
+
import { assertNoLegacyOperationLocks } from "./legacy-maintenance-boundary.js";
|
|
8
|
+
|
|
9
|
+
async function exists(filename) {
|
|
10
|
+
try { await lstat(filename); return true; }
|
|
11
|
+
catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function bootstrapFreshProject(target) {
|
|
15
|
+
const { assertStorageTopology } = await import("./topology.js");
|
|
16
|
+
assertStorageTopology(await assertSafePath(target, ".forgeloop/state.sqlite"));
|
|
17
|
+
const { LEGACY_SOURCE_ROOTS } = await import("./migration-source.js");
|
|
18
|
+
for (const relative of [...LEGACY_SOURCE_ROOTS, ".forgeloop/state.sqlite-wal", ".forgeloop/state.sqlite-shm"]) {
|
|
19
|
+
if (await exists(await assertSafePath(target, relative))) {
|
|
20
|
+
throw Object.assign(new Error("Legacy operational state requires explicit storage migration before mutation"), { code: "E_STORAGE_MIGRATION_REQUIRED" });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const { bootstrapProjectStorage } = await import("./project-bootstrap.js");
|
|
24
|
+
// Fresh state has no legacy authority to convert. Restore publication owns
|
|
25
|
+
// exclusive admission and rechecks freshness after acquiring it.
|
|
26
|
+
await bootstrapProjectStorage(target, { writersQuiesced: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Bootstrap fresh writable projects; never convert legacy state implicitly. */
|
|
30
|
+
export async function withProjectStorage(target, callback, { readOnly = false, runtimeContext = null, connectionLease = null, existingOnly = false } = {}) {
|
|
31
|
+
const owner = getStorageConnectionOwner(runtimeContext);
|
|
32
|
+
if (owner && !getOperationalStore(target)) return owner.run(target, lease => withProjectStorage(target, callback, { readOnly, connectionLease: lease, existingOnly }), { readOnly });
|
|
33
|
+
await assertStorageMaintenanceInactive(target);
|
|
34
|
+
await assertNoLegacyOperationLocks(target);
|
|
35
|
+
const marker = await readStorageVersionMarker(target);
|
|
36
|
+
if (marker?.phase === "CUTOVER_PENDING") throw Object.assign(new Error("Storage cutover requires explicit recovery before ordinary dispatch"), { code: "E_STORAGE_MIGRATION_REQUIRED" });
|
|
37
|
+
if (getOperationalStore(target)) return callback();
|
|
38
|
+
const filename = await assertSafePath(target, ".forgeloop/state.sqlite");
|
|
39
|
+
if (!(await exists(filename))) {
|
|
40
|
+
connectionLease?.assertUnselected();
|
|
41
|
+
if (marker || existingOnly) throw Object.assign(new Error("Selected SQLite storage requires its active database; filesystem fallback is forbidden"), { code: "E_STORAGE_MIGRATION_REQUIRED" });
|
|
42
|
+
if (readOnly) return callback();
|
|
43
|
+
await bootstrapFreshProject(target);
|
|
44
|
+
return withProjectStorage(target, callback, { readOnly, connectionLease, existingOnly });
|
|
45
|
+
}
|
|
46
|
+
// SQLite opens these adjacent files itself; validate their containment too.
|
|
47
|
+
await assertSafePath(target, ".forgeloop/state.sqlite-wal");
|
|
48
|
+
await assertSafePath(target, ".forgeloop/state.sqlite-shm");
|
|
49
|
+
for (const relativePath of [
|
|
50
|
+
".forgeloop/task-state",
|
|
51
|
+
".forgeloop/work-state.json",
|
|
52
|
+
".forgeloop/events.ndjson",
|
|
53
|
+
".forgeloop/.txn",
|
|
54
|
+
".forgeloop/session.json",
|
|
55
|
+
".forgeloop/sessions",
|
|
56
|
+
]) {
|
|
57
|
+
if (await exists(await assertSafePath(target, relativePath))) {
|
|
58
|
+
throw Object.assign(new Error("SQLite and writable legacy operational state cannot coexist; explicit migration is required"), { code: "E_STORAGE_MIGRATION_REQUIRED" });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const [{ openStorageDatabase, readStorageMeta }, { withOperationalStore }] = await Promise.all([
|
|
62
|
+
import("./connection.js"), import("./unit-of-work.js"),
|
|
63
|
+
]);
|
|
64
|
+
const root = await realpath(target);
|
|
65
|
+
const identity = await lstat(filename, { bigint: true });
|
|
66
|
+
const rootIdentity = await lstat(target, { bigint: true });
|
|
67
|
+
const options = { readOnly, allowSchemaUpgrade: !marker, allowOptionalIndexCreation: !readOnly,
|
|
68
|
+
projectAdmission: { root, target, dev: identity.dev, ino: identity.ino, rootIdentity } };
|
|
69
|
+
const db = connectionLease ? await connectionLease.open(filename, options) : openStorageDatabase(filename, options);
|
|
70
|
+
try {
|
|
71
|
+
if (marker) {
|
|
72
|
+
const metadata = readStorageMeta(db);
|
|
73
|
+
if (metadata.schema_version !== marker.databaseSchemaVersion || metadata.storage_version !== marker.storageVersion
|
|
74
|
+
|| metadata.storage_format !== marker.storageFormat) throw Object.assign(new Error("Active database metadata disagrees with its storage marker"), { code: "E_STORAGE_VERSION_MARKER_INVALID" });
|
|
75
|
+
}
|
|
76
|
+
return await withOperationalStore({ db, target, readOnly }, callback);
|
|
77
|
+
}
|
|
78
|
+
finally { if (!connectionLease) db.close(); }
|
|
79
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getOperationalStore } from "./operational-context.js";
|
|
2
|
+
import { needsExistingProjectScope, withExistingProjectScope } from "./existing-project-scope.js";
|
|
3
|
+
|
|
4
|
+
/** Keep committed native projections consistent without holding live locks across I/O. */
|
|
5
|
+
export async function withProjectReadSnapshot(target, read) {
|
|
6
|
+
if (await needsExistingProjectScope(target)) {
|
|
7
|
+
return withExistingProjectScope(target, () => withProjectReadSnapshot(target, read), { readOnly: true });
|
|
8
|
+
}
|
|
9
|
+
const store = getOperationalStore(target);
|
|
10
|
+
// Prepared operations must retain their staged records and transaction observations.
|
|
11
|
+
if (!store || store.hasPreparedMutations()) return read();
|
|
12
|
+
const { withStorageSnapshot, isOwnedStorageSnapshot } = await import("./snapshot.js");
|
|
13
|
+
// Reuse only a pure reader on an owned immutable copy. Writable ancestors
|
|
14
|
+
// still need detached observation scopes for later conflict checks.
|
|
15
|
+
if (store.captureObservations === false && isOwnedStorageSnapshot(store.db)) return read();
|
|
16
|
+
const { withOperationalReadSnapshot } = await import("./unit-of-work.js");
|
|
17
|
+
return withStorageSnapshot(store.db, db => withOperationalReadSnapshot({ db, target }, read));
|
|
18
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { assertSafePath, writeFileAtomic } from "../core/filesystem.js";
|
|
5
|
+
import { canonicalFingerprint } from "../core/artifacts.js";
|
|
6
|
+
import { getPackageRoot } from "../core/templates.js";
|
|
7
|
+
import { withStorageMaintenance, resumeStorageMaintenance, assertOwnedStorageMaintenance, retainStorageMaintenance, assertStorageMaintenanceOwnerContinuity } from "./maintenance.js";
|
|
8
|
+
import { backupProjectStorage, withVerifiedProjectStorageBackup } from "./project-backup.js";
|
|
9
|
+
import { validateMigrationDatabase } from "./migration-validation.js";
|
|
10
|
+
import { logicalSnapshot } from "./migration-candidate.js";
|
|
11
|
+
import { prepareActiveStorageReplacement, REPLACEMENT_ACTIVE_ROOTS } from "./restore-replacement.js";
|
|
12
|
+
import { archiveActiveStorageReplacement } from "./restore-replacement-archive.js";
|
|
13
|
+
import { publishArchivedStorageReplacement, resumeProjectStorageRestore } from "./project-restore.js";
|
|
14
|
+
import { verifyProjectRestoreJournal } from "./restore-journal.js";
|
|
15
|
+
import { MAINTENANCE_OWNER_ID } from "./maintenance-owner.js";
|
|
16
|
+
import { readStorageMetadataJson } from "./metadata-json.js";
|
|
17
|
+
import { restorePathExists } from "./restore-layout.js";
|
|
18
|
+
import { ensurePreparedRestoreSnapshot } from "./restore-preparation.js";
|
|
19
|
+
import { adoptPreparedReplacementOwner } from "./restore-replacement-owner.js";
|
|
20
|
+
import { ensurePreparedOutgoingReplacement, assertOutgoingRebuildIntent } from "./restore-replacement-preparation.js";
|
|
21
|
+
|
|
22
|
+
const invalid = message => Object.assign(new Error(message), { code: "E_STORAGE_RESTORE_INVALID" });
|
|
23
|
+
const hash = value => typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
|
|
24
|
+
|
|
25
|
+
function assertRebuildIntent(intent) {
|
|
26
|
+
if (intent.rebuild && (!MAINTENANCE_OWNER_ID.test(intent.rebuild.historyId ?? "") || typeof intent.rebuild.retainSnapshot !== "boolean"
|
|
27
|
+
|| !["RETAINING", "RETAINED"].includes(intent.rebuild.phase) || (intent.phase === "READY" && intent.rebuild.phase !== "RETAINED"))) throw invalid("Replacement snapshot retention intent is invalid");
|
|
28
|
+
assertOutgoingRebuildIntent(intent);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function verifyProjectReplacementIntent(target, operationId, { expectedOwnerId = null } = {}) {
|
|
32
|
+
if (!MAINTENANCE_OWNER_ID.test(operationId ?? "")) throw invalid("Replacement inspection requires an exact operation identity");
|
|
33
|
+
const root = await assertSafePath(target, `.forgeloop/storage-restores/${operationId}`);
|
|
34
|
+
const intent = await readStorageMetadataJson(root, "replacement-intent.json");
|
|
35
|
+
if (!intent || intent.schemaVersion !== 1 || intent.kind !== "ACTIVE_PROJECT_REPLACEMENT" || intent.operationId !== operationId
|
|
36
|
+
|| !MAINTENANCE_OWNER_ID.test(intent.ownerId ?? "") || (expectedOwnerId !== null && intent.ownerId !== expectedOwnerId)
|
|
37
|
+
|| typeof intent.source !== "string" || !path.isAbsolute(intent.source) || !hash(intent.sourceFingerprint)
|
|
38
|
+
|| !["PREPARING", "READY"].includes(intent.phase)
|
|
39
|
+
|| (intent.phase === "READY" && !hash(intent.snapshotFingerprint))
|
|
40
|
+
|| (intent.phase === "PREPARING" && intent.snapshotFingerprint !== undefined)) throw invalid("Replacement preparation identity or phase is invalid");
|
|
41
|
+
assertRebuildIntent(intent);
|
|
42
|
+
return { root, intent };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Select only the stage recorded for the explicitly requested replacement. */
|
|
46
|
+
export async function resumeActiveProjectReplacement(target, options = {}) {
|
|
47
|
+
if (options.writersQuiesced !== true) throw Object.assign(new Error("Exclude writers before replacement recovery"), { code: "E_STORAGE_MIGRATION_QUIESCENCE_REQUIRED" });
|
|
48
|
+
const selected = await verifyProjectReplacementIntent(target, options.operationId);
|
|
49
|
+
if (!await restorePathExists(selected.root, "restore-journal.json")) return resumeProjectReplacementPreparation(target, options);
|
|
50
|
+
const publication = await verifyProjectRestoreJournal(target, options.operationId);
|
|
51
|
+
if (selected.intent.phase !== "READY" || !publication.journal.replacement
|
|
52
|
+
|| selected.intent.snapshotFingerprint !== publication.journal.snapshotFingerprint
|
|
53
|
+
|| selected.intent.snapshotFingerprint !== publication.journal.replacement.incomingFingerprint) throw invalid("Replacement preparation and publication checkpoints disagree");
|
|
54
|
+
return resumeProjectStorageRestore(target, options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Resume preparation/archival before a publication journal has been created. */
|
|
58
|
+
export async function resumeProjectReplacementPreparation(target, { operationId, expectedOwnerId, writersQuiesced = false, packageRoot = getPackageRoot() } = {}) {
|
|
59
|
+
if (writersQuiesced !== true) throw Object.assign(new Error("Exclude writers before replacement recovery"), { code: "E_STORAGE_MIGRATION_QUIESCENCE_REQUIRED" });
|
|
60
|
+
const before = await verifyProjectReplacementIntent(target, operationId);
|
|
61
|
+
await assertStorageMaintenanceOwnerContinuity(target, { expectedOwnerId, recordedOwnerId: before.intent.ownerId });
|
|
62
|
+
if (await restorePathExists(before.root, "restore-journal.json")) throw invalid("Replacement publication intent already exists; use publication resume");
|
|
63
|
+
return resumeStorageMaintenance(target, { expectedOwnerId, writersQuiesced }, async () => {
|
|
64
|
+
await retainStorageMaintenance(target);
|
|
65
|
+
const current = await verifyProjectReplacementIntent(target, operationId);
|
|
66
|
+
if (canonicalFingerprint(current.intent) !== canonicalFingerprint(before.intent)) throw invalid("Replacement intent changed during owner adoption");
|
|
67
|
+
await assertStorageMaintenanceOwnerContinuity(target, { expectedOwnerId: await assertOwnedStorageMaintenance(target), recordedOwnerId: current.intent.ownerId });
|
|
68
|
+
const intent = current.intent;
|
|
69
|
+
intent.ownerId = await assertOwnedStorageMaintenance(target);
|
|
70
|
+
const persist = async () => {
|
|
71
|
+
await assertOwnedStorageMaintenance(target);
|
|
72
|
+
await writeFileAtomic(await assertSafePath(current.root, "replacement-intent.json"), `${JSON.stringify(intent, null, 2)}\n`);
|
|
73
|
+
};
|
|
74
|
+
await persist();
|
|
75
|
+
await ensurePreparedOutgoingReplacement(target, current.root, intent, { writersQuiesced, packageRoot });
|
|
76
|
+
await adoptPreparedReplacementOwner(target, operationId, { expectedOwnerId });
|
|
77
|
+
if (intent.phase === "PREPARING") {
|
|
78
|
+
await ensurePreparedRestoreSnapshot(target, current.root, intent, { journalFilename: "replacement-intent.json" });
|
|
79
|
+
await withVerifiedProjectStorageBackup(await assertSafePath(current.root, "snapshot"), async retained => {
|
|
80
|
+
await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot });
|
|
81
|
+
await withVerifiedProjectStorageBackup(intent.source, source => {
|
|
82
|
+
if (canonicalFingerprint(source.manifest) !== intent.sourceFingerprint
|
|
83
|
+
|| canonicalFingerprint(logicalSnapshot(source.db)) !== canonicalFingerprint(logicalSnapshot(retained.db))) throw invalid("Replacement recovery snapshot differs from recorded source");
|
|
84
|
+
});
|
|
85
|
+
intent.snapshotFingerprint = canonicalFingerprint(retained.manifest);
|
|
86
|
+
});
|
|
87
|
+
intent.phase = "READY";
|
|
88
|
+
await persist();
|
|
89
|
+
}
|
|
90
|
+
await withVerifiedProjectStorageBackup(await assertSafePath(current.root, "snapshot"), retained => {
|
|
91
|
+
if (canonicalFingerprint(retained.manifest) !== intent.snapshotFingerprint) throw invalid("Replacement incoming snapshot changed after preparation");
|
|
92
|
+
});
|
|
93
|
+
await archiveActiveStorageReplacement(target, operationId, { packageRoot, expectedOwnerId });
|
|
94
|
+
return publishArchivedStorageReplacement(target, operationId, { packageRoot });
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Initial replacement orchestration. Every outgoing byte remains independently retained. */
|
|
99
|
+
export async function replaceActiveProjectStorage(source, target, { writersQuiesced = false, packageRoot = getPackageRoot() } = {}) {
|
|
100
|
+
if (writersQuiesced !== true) throw Object.assign(new Error("Exclude writers and close their database connections before active replacement"), { code: "E_STORAGE_MIGRATION_QUIESCENCE_REQUIRED" });
|
|
101
|
+
return withVerifiedProjectStorageBackup(source, async ({ db, root, manifest }) => {
|
|
102
|
+
await validateMigrationDatabase(db, { target: root, packageRoot });
|
|
103
|
+
const relative = path.relative(path.resolve(target), root).split(path.sep).join("/");
|
|
104
|
+
if (REPLACEMENT_ACTIVE_ROOTS.some(name => relative === name || relative.startsWith(`${name}/`))) throw invalid("Replacement source cannot be archived outgoing operational state");
|
|
105
|
+
return withStorageMaintenance(target, async () => {
|
|
106
|
+
await retainStorageMaintenance(target);
|
|
107
|
+
const operationId = randomUUID();
|
|
108
|
+
const operationRoot = await assertSafePath(target, `.forgeloop/storage-restores/${operationId}`);
|
|
109
|
+
await mkdir(path.dirname(operationRoot), { recursive: true });
|
|
110
|
+
await mkdir(operationRoot);
|
|
111
|
+
const intent = { schemaVersion: 1, kind: "ACTIVE_PROJECT_REPLACEMENT", operationId, ownerId: await assertOwnedStorageMaintenance(target),
|
|
112
|
+
source: root, sourceFingerprint: canonicalFingerprint(manifest), phase: "PREPARING" };
|
|
113
|
+
const persist = async () => {
|
|
114
|
+
await assertOwnedStorageMaintenance(target);
|
|
115
|
+
await writeFileAtomic(await assertSafePath(operationRoot, "replacement-intent.json"), `${JSON.stringify(intent, null, 2)}\n`);
|
|
116
|
+
};
|
|
117
|
+
await persist();
|
|
118
|
+
await prepareActiveStorageReplacement(target, operationId, { writersQuiesced, packageRoot });
|
|
119
|
+
const snapshot = await assertSafePath(operationRoot, "snapshot");
|
|
120
|
+
await backupProjectStorage(db, root, snapshot);
|
|
121
|
+
await withVerifiedProjectStorageBackup(snapshot, async retained => {
|
|
122
|
+
await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot });
|
|
123
|
+
await withVerifiedProjectStorageBackup(root, current => {
|
|
124
|
+
if (canonicalFingerprint(current.manifest) !== intent.sourceFingerprint
|
|
125
|
+
|| canonicalFingerprint(logicalSnapshot(current.db)) !== canonicalFingerprint(logicalSnapshot(retained.db))) throw invalid("Incoming source changed during replacement preparation");
|
|
126
|
+
});
|
|
127
|
+
intent.snapshotFingerprint = canonicalFingerprint(retained.manifest);
|
|
128
|
+
});
|
|
129
|
+
intent.phase = "READY";
|
|
130
|
+
await persist();
|
|
131
|
+
await archiveActiveStorageReplacement(target, operationId, { packageRoot });
|
|
132
|
+
// Publication owns the terminal validator and admission release decision.
|
|
133
|
+
const result = await publishArchivedStorageReplacement(target, operationId, { packageRoot });
|
|
134
|
+
return { ...result, operationId, replacementIntent: await assertSafePath(operationRoot, "replacement-intent.json") };
|
|
135
|
+
}, { retainOnError: true });
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { randomUUID } 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 { assertSafePath, writeFileAtomic } from "../core/filesystem.js";
|
|
6
|
+
import { canonicalFingerprint } from "../core/artifacts.js";
|
|
7
|
+
import { getPackageRoot } from "../core/templates.js";
|
|
8
|
+
import { LEGACY_SOURCE_ROOTS } from "./migration-source.js";
|
|
9
|
+
import { assertNoLegacyOperationLocks } from "./legacy-maintenance-boundary.js";
|
|
10
|
+
import { withStorageMaintenance, resumeStorageMaintenance, assertOwnedStorageMaintenance, retainStorageMaintenance, allowStorageMaintenanceRelease, assertStorageMaintenanceOwnerContinuity } from "./maintenance.js";
|
|
11
|
+
import { readStorageVersionMarker, writePendingStorageVersionMarker, activateStorageVersionMarker } from "./storage-marker.js";
|
|
12
|
+
import { backupProjectStorage, withVerifiedProjectStorageBackup } from "./project-backup.js";
|
|
13
|
+
import { backupStorageDatabase } from "./backup.js";
|
|
14
|
+
import { openStorageDatabase, readStorageMeta, checkStorageIntegrity } from "./connection.js";
|
|
15
|
+
import { validateMigrationDatabase } from "./migration-validation.js";
|
|
16
|
+
import { iterateAttachmentReferences } from "./attachment-references.js";
|
|
17
|
+
import { publishAttachmentFile, withVerifiedAttachmentFile, verifyAttachmentFile } from "./attachment-files.js";
|
|
18
|
+
import { logicalSnapshot } from "./migration-candidate.js";
|
|
19
|
+
import { verifyProjectRestoreJournal } from "./restore-journal.js";
|
|
20
|
+
import { assertRestorePublicationLayout, restorePathExists } from "./restore-layout.js";
|
|
21
|
+
import { verifyActiveProjectRestore } from "./restore-terminal.js";
|
|
22
|
+
import { ensurePreparedRestoreSnapshot } from "./restore-preparation.js";
|
|
23
|
+
import { verifyActiveStorageReplacementArchive, verifyRetainedStorageReplacementArchive } from "./restore-replacement-archive.js";
|
|
24
|
+
import { readStorageMetadataJson } from "./metadata-json.js";
|
|
25
|
+
|
|
26
|
+
/** Publish the verified incoming snapshot only after complete outgoing archival. */
|
|
27
|
+
export async function publishArchivedStorageReplacement(target, operationId, { packageRoot = getPackageRoot() } = {}) {
|
|
28
|
+
const ownerId = await assertOwnedStorageMaintenance(target);
|
|
29
|
+
await retainStorageMaintenance(target);
|
|
30
|
+
const operationRoot = await assertSafePath(target, `.forgeloop/storage-restores/${operationId}`);
|
|
31
|
+
const journalPath = await assertSafePath(operationRoot, "restore-journal.json");
|
|
32
|
+
let journal = await readStorageMetadataJson(operationRoot, "restore-journal.json", { optional: true });
|
|
33
|
+
if (!journal) {
|
|
34
|
+
const archived = await verifyActiveStorageReplacementArchive(target, operationId, { packageRoot });
|
|
35
|
+
if (!archived.archived || archived.journal.ownerId !== ownerId) throw invalid("Replacement publication requires complete archival by the current owner");
|
|
36
|
+
await assertFreshOperationalState(target);
|
|
37
|
+
await withVerifiedProjectStorageBackup(archived.incomingSnapshot, async ({ db, root, manifest }) => {
|
|
38
|
+
journal = { schemaVersion: 1, kind: "PROJECT_RESTORE", operationId, ownerId, source: root,
|
|
39
|
+
sourceFingerprint: canonicalFingerprint(manifest), phase: "READY", snapshotFingerprint: canonicalFingerprint(manifest),
|
|
40
|
+
replacement: { outgoingFingerprint: archived.journal.outgoingFingerprint, incomingFingerprint: archived.journal.incomingFingerprint },
|
|
41
|
+
binding: { operationId, databaseSchemaVersion: readStorageMeta(db).schema_version,
|
|
42
|
+
sourceInventoryFingerprint: canonicalFingerprint({ database: manifest.databaseSha256, inventory: manifest.inventorySha256, references: manifest.references }) } };
|
|
43
|
+
await assertOwnedStorageMaintenance(target);
|
|
44
|
+
await writeFileAtomic(journalPath, `${JSON.stringify(journal, null, 2)}\n`);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const verified = await verifyProjectRestoreJournal(target, operationId, { expectedOwnerId: ownerId });
|
|
48
|
+
if (!verified.journal.replacement) throw invalid("Selected restore is not an archived replacement");
|
|
49
|
+
await verifyRetainedStorageReplacementArchive(target, operationId, { ...verified.journal.replacement, packageRoot });
|
|
50
|
+
const result = verified.journal.phase === "ACTIVE"
|
|
51
|
+
? await verifyActiveProjectRestore(target, operationId, { expectedOwnerId: ownerId, packageRoot })
|
|
52
|
+
: await publishReadyRestore(target, operationRoot, verified.journal, { packageRoot });
|
|
53
|
+
await verifyActiveProjectRestore(target, operationId, { expectedOwnerId: ownerId, packageRoot });
|
|
54
|
+
await allowStorageMaintenanceRelease(target);
|
|
55
|
+
return { ...result, replaced: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function invalid(message) { return Object.assign(new Error(message), { code: "E_STORAGE_RESTORE_INVALID" }); }
|
|
59
|
+
|
|
60
|
+
export async function assertFreshOperationalState(target) {
|
|
61
|
+
await assertNoLegacyOperationLocks(target, { code: "E_STORAGE_RESTORE_INVALID" });
|
|
62
|
+
if (await readStorageVersionMarker(target)) throw invalid("Restore activation requires a project without an existing storage marker");
|
|
63
|
+
for (const relative of [...LEGACY_SOURCE_ROOTS, ".forgeloop/state.sqlite", ".forgeloop/state.sqlite-wal", ".forgeloop/state.sqlite-shm"]) {
|
|
64
|
+
try { await lstat(await assertSafePath(target, relative)); }
|
|
65
|
+
catch (error) { if (error.code === "ENOENT") continue; throw error; }
|
|
66
|
+
throw invalid(`Restore activation refuses existing operational state: ${relative}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Restore a verified independent snapshot into an operationally fresh project. */
|
|
71
|
+
export async function restoreProjectStorageToFreshProject(source, target, { writersQuiesced = false, packageRoot = getPackageRoot() } = {}) {
|
|
72
|
+
if (writersQuiesced !== true) throw Object.assign(new Error("Exclude all writers before restore activation"), { code: "E_STORAGE_MIGRATION_QUIESCENCE_REQUIRED" });
|
|
73
|
+
// Verify source before acquiring exclusion or allocating restore evidence.
|
|
74
|
+
return withVerifiedProjectStorageBackup(source, async ({ db, root, manifest: sourceManifest }) => {
|
|
75
|
+
await assertFreshOperationalState(target);
|
|
76
|
+
await mkdir(await assertSafePath(target, ".forgeloop"), { recursive: true });
|
|
77
|
+
return withStorageMaintenance(target, async () => {
|
|
78
|
+
await assertFreshOperationalState(target);
|
|
79
|
+
const operationId = randomUUID();
|
|
80
|
+
const operationRoot = await assertSafePath(target, `.forgeloop/storage-restores/${operationId}`);
|
|
81
|
+
await mkdir(path.dirname(operationRoot), { recursive: true });
|
|
82
|
+
await mkdir(operationRoot);
|
|
83
|
+
const journalPath = await assertSafePath(operationRoot, "restore-journal.json");
|
|
84
|
+
const journal = { schemaVersion: 1, kind: "PROJECT_RESTORE", operationId, ownerId: await assertOwnedStorageMaintenance(target),
|
|
85
|
+
source: root, sourceFingerprint: canonicalFingerprint(sourceManifest), phase: "PREPARING" };
|
|
86
|
+
const persist = () => writeFileAtomic(journalPath, `${JSON.stringify(journal, null, 2)}\n`);
|
|
87
|
+
await persist();
|
|
88
|
+
const retained = await assertSafePath(operationRoot, "snapshot");
|
|
89
|
+
await backupProjectStorage(db, root, retained);
|
|
90
|
+
return withVerifiedProjectStorageBackup(retained, async ({ db: snapshot, manifest }) => {
|
|
91
|
+
await withVerifiedProjectStorageBackup(root, current => {
|
|
92
|
+
if (canonicalFingerprint(current.manifest) !== canonicalFingerprint(sourceManifest)
|
|
93
|
+
|| canonicalFingerprint(logicalSnapshot(current.db)) !== canonicalFingerprint(logicalSnapshot(snapshot))) throw invalid("Source backup changed during restore preparation");
|
|
94
|
+
});
|
|
95
|
+
await validateMigrationDatabase(snapshot, { target: retained, packageRoot });
|
|
96
|
+
const binding = { operationId, databaseSchemaVersion: readStorageMeta(snapshot).schema_version,
|
|
97
|
+
sourceInventoryFingerprint: canonicalFingerprint({ database: manifest.databaseSha256, inventory: manifest.inventorySha256, references: manifest.references }) };
|
|
98
|
+
Object.assign(journal, { phase: "READY", binding, snapshotFingerprint: canonicalFingerprint(manifest) });
|
|
99
|
+
await persist();
|
|
100
|
+
return publishReadyRestore(target, operationRoot, journal, { packageRoot });
|
|
101
|
+
});
|
|
102
|
+
}, { retainOnError: true });
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function publishReadyRestore(target, operationRoot, journal, { packageRoot }) {
|
|
107
|
+
if (!["READY", "PUBLISHING"].includes(journal.phase)) throw invalid("Restore publication requires a validated READY or PUBLISHING checkpoint");
|
|
108
|
+
if (journal.phase === "READY") await assertFreshOperationalState(target);
|
|
109
|
+
const retained = await assertSafePath(operationRoot, "snapshot");
|
|
110
|
+
const journalPath = await assertSafePath(operationRoot, "restore-journal.json");
|
|
111
|
+
const persist = () => writeFileAtomic(journalPath, `${JSON.stringify(journal, null, 2)}\n`);
|
|
112
|
+
const { operationId, binding } = journal;
|
|
113
|
+
return withVerifiedProjectStorageBackup(retained, async ({ db: snapshot, manifest }) => {
|
|
114
|
+
await validateMigrationDatabase(snapshot, { target: retained, packageRoot });
|
|
115
|
+
await assertOwnedStorageMaintenance(target);
|
|
116
|
+
await verifyProjectRestoreJournal(target, operationId, { expectedOwnerId: await assertOwnedStorageMaintenance(target) });
|
|
117
|
+
await assertRestorePublicationLayout(target, snapshot);
|
|
118
|
+
const existingMarker = await readStorageVersionMarker(target);
|
|
119
|
+
const alreadyActive = existingMarker?.phase === "ACTIVE";
|
|
120
|
+
const filename = await assertSafePath(target, ".forgeloop/state.sqlite");
|
|
121
|
+
const existingDatabase = await restorePathExists(target, ".forgeloop/state.sqlite");
|
|
122
|
+
if (alreadyActive && !existingDatabase) throw invalid("An active restore database cannot be automatically recreated");
|
|
123
|
+
if (existingDatabase) {
|
|
124
|
+
for (const suffix of ["-wal", "-shm"]) await assertSafePath(target, `.forgeloop/state.sqlite${suffix}`);
|
|
125
|
+
const current = openStorageDatabase(filename, { readOnly: true });
|
|
126
|
+
try {
|
|
127
|
+
if (!checkStorageIntegrity(current).ok || canonicalFingerprint(logicalSnapshot(current)) !== canonicalFingerprint(logicalSnapshot(snapshot))) throw invalid("Existing restore database differs; automatic overwrite is forbidden");
|
|
128
|
+
if (alreadyActive) for (const reference of iterateAttachmentReferences(current)) await verifyAttachmentFile(target, reference);
|
|
129
|
+
} finally { current.close(); }
|
|
130
|
+
}
|
|
131
|
+
journal.phase = "PUBLISHING";
|
|
132
|
+
await persist();
|
|
133
|
+
if (!alreadyActive) await writePendingStorageVersionMarker(target, binding);
|
|
134
|
+
const temporaryRoot = await assertSafePath(retained, "activation-attachments");
|
|
135
|
+
await mkdir(temporaryRoot, { recursive: true });
|
|
136
|
+
for (const reference of alreadyActive ? [] : iterateAttachmentReferences(snapshot)) {
|
|
137
|
+
await assertOwnedStorageMaintenance(target);
|
|
138
|
+
const copied = await withVerifiedAttachmentFile(retained, reference, filename => publishAttachmentFile(target, createReadStream(filename), { db: snapshot, temporaryRoot }));
|
|
139
|
+
if (copied.sha256 !== reference.sha256 || copied.size !== reference.size) throw invalid("Restored attachment differs from retained snapshot");
|
|
140
|
+
}
|
|
141
|
+
if (!existingDatabase) await backupStorageDatabase(snapshot, filename);
|
|
142
|
+
const restored = openStorageDatabase(filename, { readOnly: true });
|
|
143
|
+
try {
|
|
144
|
+
if (!checkStorageIntegrity(restored).ok || canonicalFingerprint(logicalSnapshot(restored)) !== canonicalFingerprint(logicalSnapshot(snapshot))) throw invalid("Restored canonical state differs from retained snapshot");
|
|
145
|
+
await validateMigrationDatabase(restored, { target, packageRoot });
|
|
146
|
+
for (const reference of iterateAttachmentReferences(restored)) await verifyAttachmentFile(target, reference);
|
|
147
|
+
} finally { restored.close(); }
|
|
148
|
+
await assertOwnedStorageMaintenance(target);
|
|
149
|
+
const marker = alreadyActive ? existingMarker : await activateStorageVersionMarker(target, binding);
|
|
150
|
+
journal.phase = "ACTIVE";
|
|
151
|
+
await persist();
|
|
152
|
+
await verifyProjectRestoreJournal(target, operationId, { expectedOwnerId: await assertOwnedStorageMaintenance(target) });
|
|
153
|
+
return { restored: true, active: true, currentStateVerified: true, path: filename, retainedSnapshot: retained, references: manifest.references, marker, journalPath };
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Resume a validated prepublication snapshot after confirmed local owner death. */
|
|
158
|
+
export async function resumeProjectStorageRestore(target, { operationId, expectedOwnerId, writersQuiesced = false, packageRoot = getPackageRoot() } = {}) {
|
|
159
|
+
if (writersQuiesced !== true) throw Object.assign(new Error("Exclude all writers before restore resume"), { code: "E_STORAGE_MIGRATION_QUIESCENCE_REQUIRED" });
|
|
160
|
+
const verified = await verifyProjectRestoreJournal(target, operationId);
|
|
161
|
+
await assertStorageMaintenanceOwnerContinuity(target, { expectedOwnerId, recordedOwnerId: verified.journal.ownerId });
|
|
162
|
+
if (!["PREPARING", "READY", "PUBLISHING", "ACTIVE"].includes(verified.journal.phase)) throw invalid("Restore checkpoint does not support resume");
|
|
163
|
+
if (["PREPARING", "READY"].includes(verified.journal.phase)) await assertFreshOperationalState(target);
|
|
164
|
+
return resumeStorageMaintenance(target, { expectedOwnerId, writersQuiesced }, async () => {
|
|
165
|
+
const current = await verifyProjectRestoreJournal(target, operationId);
|
|
166
|
+
if (canonicalFingerprint(current.journal) !== canonicalFingerprint(verified.journal)) throw invalid("Restore journal changed during owner adoption");
|
|
167
|
+
await assertStorageMaintenanceOwnerContinuity(target, { expectedOwnerId: await assertOwnedStorageMaintenance(target), recordedOwnerId: current.journal.ownerId });
|
|
168
|
+
current.journal.ownerId = await assertOwnedStorageMaintenance(target);
|
|
169
|
+
await writeFileAtomic(await assertSafePath(current.root, "restore-journal.json"), `${JSON.stringify(current.journal, null, 2)}\n`);
|
|
170
|
+
if (current.journal.replacement) return publishArchivedStorageReplacement(target, operationId, { packageRoot });
|
|
171
|
+
if (current.journal.phase === "PREPARING") await validatePreparedRestore(target, current.root, current.journal, packageRoot);
|
|
172
|
+
if (current.journal.phase === "ACTIVE") {
|
|
173
|
+
const result = await verifyActiveProjectRestore(target, operationId, { expectedOwnerId: current.journal.ownerId, packageRoot });
|
|
174
|
+
await assertOwnedStorageMaintenance(target);
|
|
175
|
+
return { ...result, path: await assertSafePath(target, ".forgeloop/state.sqlite"), retainedSnapshot: current.snapshot,
|
|
176
|
+
journalPath: await assertSafePath(current.root, "restore-journal.json"), marker: current.marker };
|
|
177
|
+
}
|
|
178
|
+
return publishReadyRestore(target, current.root, current.journal, { packageRoot });
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function validatePreparedRestore(target, root, journal, packageRoot) {
|
|
183
|
+
await ensurePreparedRestoreSnapshot(target, root, journal);
|
|
184
|
+
const snapshotPath = await assertSafePath(root, "snapshot");
|
|
185
|
+
await withVerifiedProjectStorageBackup(snapshotPath, async ({ db: snapshot, manifest }) => {
|
|
186
|
+
await withVerifiedProjectStorageBackup(journal.source, async source => {
|
|
187
|
+
if (canonicalFingerprint(source.manifest) !== journal.sourceFingerprint
|
|
188
|
+
|| canonicalFingerprint(logicalSnapshot(source.db)) !== canonicalFingerprint(logicalSnapshot(snapshot))) throw invalid("Prepared restore differs from its recorded source backup");
|
|
189
|
+
await validateMigrationDatabase(snapshot, { target: snapshotPath, packageRoot });
|
|
190
|
+
});
|
|
191
|
+
await assertOwnedStorageMaintenance(target);
|
|
192
|
+
Object.assign(journal, { phase: "READY", snapshotFingerprint: canonicalFingerprint(manifest), binding: {
|
|
193
|
+
operationId: journal.operationId, databaseSchemaVersion: readStorageMeta(snapshot).schema_version,
|
|
194
|
+
sourceInventoryFingerprint: canonicalFingerprint({ database: manifest.databaseSha256, inventory: manifest.inventorySha256, references: manifest.references }) } });
|
|
195
|
+
await writeFileAtomic(await assertSafePath(root, "restore-journal.json"), `${JSON.stringify(journal, null, 2)}\n`);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function resumeReadyProjectStorageRestore(target, options = {}) {
|
|
200
|
+
const verified = await verifyProjectRestoreJournal(target, options.operationId);
|
|
201
|
+
if (verified.journal.phase !== "READY") throw invalid("This compatibility entry point requires READY");
|
|
202
|
+
return resumeProjectStorageRestore(target, options);
|
|
203
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { taskActionPath, taskApprovalPath, taskExecutionPath } from "../core/task-paths.js";
|
|
2
|
+
import { putAction, putApproval, putExecution } from "./repository.js";
|
|
3
|
+
|
|
4
|
+
function definition({ table, idColumn, payloadId, pathBuilder, writer, conflictCode = null }) {
|
|
5
|
+
return Object.freeze({ table, idColumn, payloadId, pathBuilder, writer, conflictCode });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The indexed operational records share one namespace contract. Keep their
|
|
10
|
+
* table columns, payload identity field, canonical path builder, and conflict
|
|
11
|
+
* classification together so reads, staging, listing, and commit dispatch do
|
|
12
|
+
* not grow separate action/approval/execution copies.
|
|
13
|
+
*/
|
|
14
|
+
export const RECORD_DEFINITIONS = Object.freeze({
|
|
15
|
+
action: definition({
|
|
16
|
+
table: "actions",
|
|
17
|
+
idColumn: "action_id",
|
|
18
|
+
payloadId: "actionId",
|
|
19
|
+
pathBuilder: taskActionPath,
|
|
20
|
+
writer: putAction,
|
|
21
|
+
conflictCode: "E_ACTION_STATE_MISMATCH",
|
|
22
|
+
}),
|
|
23
|
+
approval: definition({
|
|
24
|
+
table: "approvals",
|
|
25
|
+
idColumn: "approval_id",
|
|
26
|
+
payloadId: "approvalId",
|
|
27
|
+
pathBuilder: taskApprovalPath,
|
|
28
|
+
writer: putApproval,
|
|
29
|
+
conflictCode: "E_APPROVAL_ALREADY_RESOLVED",
|
|
30
|
+
}),
|
|
31
|
+
execution: definition({
|
|
32
|
+
table: "executions",
|
|
33
|
+
idColumn: "execution_id",
|
|
34
|
+
payloadId: "executionId",
|
|
35
|
+
pathBuilder: taskExecutionPath,
|
|
36
|
+
writer: putExecution,
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** Native artifact schemas whose canonical path is an indexed record table. */
|
|
41
|
+
export const RECORD_SCHEMA_KINDS = Object.freeze({
|
|
42
|
+
action: "action",
|
|
43
|
+
approval: "approval",
|
|
44
|
+
execution: "execution",
|
|
45
|
+
});
|