@cleocode/core 2026.10.5 → 2026.10.6
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/dist/bootstrap.d.ts +13 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +21 -32
- package/dist/bootstrap.js.map +1 -1
- package/dist/build-provenance.json +2 -2
- package/dist/cloud/nexus-cloud-status.d.ts.map +1 -1
- package/dist/cloud/nexus-cloud-status.js +3 -1
- package/dist/cloud/nexus-cloud-status.js.map +1 -1
- package/dist/cloud/nexus-device.d.ts +1 -1
- package/dist/cloud/nexus-presence-refresh.d.ts.map +1 -1
- package/dist/cloud/nexus-presence-refresh.js +20 -13
- package/dist/cloud/nexus-presence-refresh.js.map +1 -1
- package/dist/cloud/nexus-vault.d.ts.map +1 -1
- package/dist/cloud/nexus-vault.js +4 -1
- package/dist/cloud/nexus-vault.js.map +1 -1
- package/dist/docs/derive-links.d.ts +69 -0
- package/dist/docs/derive-links.d.ts.map +1 -0
- package/dist/docs/derive-links.js +85 -0
- package/dist/docs/derive-links.js.map +1 -0
- package/dist/docs/docs-generator.d.ts.map +1 -1
- package/dist/docs/docs-generator.js +2 -0
- package/dist/docs/docs-generator.js.map +1 -1
- package/dist/docs/docs-update.d.ts.map +1 -1
- package/dist/docs/docs-update.js +24 -4
- package/dist/docs/docs-update.js.map +1 -1
- package/dist/docs/fetch-result.d.ts +77 -0
- package/dist/docs/fetch-result.d.ts.map +1 -0
- package/dist/docs/fetch-result.js +132 -0
- package/dist/docs/fetch-result.js.map +1 -0
- package/dist/docs/supersede.d.ts.map +1 -1
- package/dist/docs/supersede.js +9 -0
- package/dist/docs/supersede.js.map +1 -1
- package/dist/doctor/db-substrate.d.ts.map +1 -1
- package/dist/doctor/db-substrate.js +64 -29
- package/dist/doctor/db-substrate.js.map +1 -1
- package/dist/doctor/row-identity-refill.d.ts.map +1 -1
- package/dist/doctor/row-identity-refill.js +25 -10
- package/dist/doctor/row-identity-refill.js.map +1 -1
- package/dist/doctor/row-identity.d.ts.map +1 -1
- package/dist/doctor/row-identity.js +8 -2
- package/dist/doctor/row-identity.js.map +1 -1
- package/dist/doctor/sync-repair.d.ts +5 -0
- package/dist/doctor/sync-repair.d.ts.map +1 -1
- package/dist/doctor/sync-repair.js +11 -0
- package/dist/doctor/sync-repair.js.map +1 -1
- package/dist/doctor/sync-triggers.d.ts.map +1 -1
- package/dist/doctor/sync-triggers.js +3 -17
- package/dist/doctor/sync-triggers.js.map +1 -1
- package/dist/hygiene/validate-spawn-readiness.d.ts +6 -1
- package/dist/hygiene/validate-spawn-readiness.d.ts.map +1 -1
- package/dist/hygiene/validate-spawn-readiness.js +11 -6
- package/dist/hygiene/validate-spawn-readiness.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +10 -1
- package/dist/init.js.map +1 -1
- package/dist/injection.d.ts +37 -12
- package/dist/injection.d.ts.map +1 -1
- package/dist/injection.js +152 -35
- package/dist/injection.js.map +1 -1
- package/dist/memory/brain-search.d.ts.map +1 -1
- package/dist/memory/brain-search.js +7 -32
- package/dist/memory/brain-search.js.map +1 -1
- package/dist/memory/pipeline-manifest-sqlite.d.ts +2 -0
- package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
- package/dist/memory/pipeline-manifest-sqlite.js +51 -9
- package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
- package/dist/nexus/assessment-projection.d.ts +96 -1
- package/dist/nexus/assessment-projection.d.ts.map +1 -1
- package/dist/nexus/assessment-projection.js +135 -0
- package/dist/nexus/assessment-projection.js.map +1 -1
- package/dist/nexus/assessment-store.d.ts +54 -0
- package/dist/nexus/assessment-store.d.ts.map +1 -1
- package/dist/nexus/assessment-store.js +161 -2
- package/dist/nexus/assessment-store.js.map +1 -1
- package/dist/nexus/knowledge.d.ts +19 -0
- package/dist/nexus/knowledge.d.ts.map +1 -1
- package/dist/nexus/knowledge.js +56 -2
- package/dist/nexus/knowledge.js.map +1 -1
- package/dist/orchestrate/spawn-ops.d.ts.map +1 -1
- package/dist/orchestrate/spawn-ops.js +12 -1
- package/dist/orchestrate/spawn-ops.js.map +1 -1
- package/dist/release/commit-presence.d.ts +24 -0
- package/dist/release/commit-presence.d.ts.map +1 -1
- package/dist/release/commit-presence.js +31 -0
- package/dist/release/commit-presence.js.map +1 -1
- package/dist/release/plan.d.ts +8 -6
- package/dist/release/plan.d.ts.map +1 -1
- package/dist/release/plan.js +11 -6
- package/dist/release/plan.js.map +1 -1
- package/dist/release/reconcile.d.ts.map +1 -1
- package/dist/release/reconcile.js +10 -8
- package/dist/release/reconcile.js.map +1 -1
- package/dist/release/verify-provenance.d.ts.map +1 -1
- package/dist/release/verify-provenance.js +6 -8
- package/dist/release/verify-provenance.js.map +1 -1
- package/dist/resources/admission-ledger.d.ts +66 -0
- package/dist/resources/admission-ledger.d.ts.map +1 -1
- package/dist/resources/admission-ledger.js +145 -6
- package/dist/resources/admission-ledger.js.map +1 -1
- package/dist/resources/run-class.d.ts +71 -0
- package/dist/resources/run-class.d.ts.map +1 -1
- package/dist/resources/run-class.js +203 -0
- package/dist/resources/run-class.js.map +1 -1
- package/dist/resources/run-governed.d.ts +2 -0
- package/dist/resources/run-governed.d.ts.map +1 -1
- package/dist/resources/run-governed.js +1 -0
- package/dist/resources/run-governed.js.map +1 -1
- package/dist/scaffold/ensure-config.d.ts +20 -9
- package/dist/scaffold/ensure-config.d.ts.map +1 -1
- package/dist/scaffold/ensure-config.js +56 -23
- package/dist/scaffold/ensure-config.js.map +1 -1
- package/dist/scaffold/ensure-skills.d.ts +10 -0
- package/dist/scaffold/ensure-skills.d.ts.map +1 -1
- package/dist/scaffold/ensure-skills.js +47 -3
- package/dist/scaffold/ensure-skills.js.map +1 -1
- package/dist/scaffold/upgrade-file-journal.d.ts +79 -0
- package/dist/scaffold/upgrade-file-journal.d.ts.map +1 -0
- package/dist/scaffold/upgrade-file-journal.js +111 -0
- package/dist/scaffold/upgrade-file-journal.js.map +1 -0
- package/dist/session/engine-ops.d.ts.map +1 -1
- package/dist/session/engine-ops.js +10 -2
- package/dist/session/engine-ops.js.map +1 -1
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +3 -0
- package/dist/sessions/index.js.map +1 -1
- package/dist/spawn/worktree-lock-holder.d.ts +59 -1
- package/dist/spawn/worktree-lock-holder.d.ts.map +1 -1
- package/dist/spawn/worktree-lock-holder.js +114 -0
- package/dist/spawn/worktree-lock-holder.js.map +1 -1
- package/dist/store/attachment-store.d.ts.map +1 -1
- package/dist/store/attachment-store.js +38 -0
- package/dist/store/attachment-store.js.map +1 -1
- package/dist/store/backup-recover.d.ts +4 -4
- package/dist/store/backup-recover.d.ts.map +1 -1
- package/dist/store/backup-recover.js +11 -4
- package/dist/store/backup-recover.js.map +1 -1
- package/dist/store/backup-sidecar.d.ts +19 -1
- package/dist/store/backup-sidecar.d.ts.map +1 -1
- package/dist/store/backup-sidecar.js +57 -6
- package/dist/store/backup-sidecar.js.map +1 -1
- package/dist/store/db-helpers.js +1 -1
- package/dist/store/display-id-alias.js +1 -1
- package/dist/store/display-id-alias.js.map +1 -1
- package/dist/store/dual-scope-db.d.ts +2 -0
- package/dist/store/dual-scope-db.d.ts.map +1 -1
- package/dist/store/dual-scope-db.js +28 -16
- package/dist/store/dual-scope-db.js.map +1 -1
- package/dist/store/exodus/archive.d.ts +42 -2
- package/dist/store/exodus/archive.d.ts.map +1 -1
- package/dist/store/exodus/archive.js +81 -4
- package/dist/store/exodus/archive.js.map +1 -1
- package/dist/store/exodus/bare-family.d.ts +104 -0
- package/dist/store/exodus/bare-family.d.ts.map +1 -0
- package/dist/store/exodus/bare-family.js +470 -0
- package/dist/store/exodus/bare-family.js.map +1 -0
- package/dist/store/exodus/column-transforms.d.ts.map +1 -1
- package/dist/store/exodus/column-transforms.js +15 -8
- package/dist/store/exodus/column-transforms.js.map +1 -1
- package/dist/store/exodus/health.d.ts +2 -0
- package/dist/store/exodus/health.d.ts.map +1 -1
- package/dist/store/exodus/health.js +7 -1
- package/dist/store/exodus/health.js.map +1 -1
- package/dist/store/exodus/index.d.ts +2 -2
- package/dist/store/exodus/index.d.ts.map +1 -1
- package/dist/store/exodus/index.js +2 -2
- package/dist/store/exodus/index.js.map +1 -1
- package/dist/store/exodus/migrate.d.ts.map +1 -1
- package/dist/store/exodus/migrate.js +30 -1
- package/dist/store/exodus/migrate.js.map +1 -1
- package/dist/store/exodus/on-open.d.ts +11 -0
- package/dist/store/exodus/on-open.d.ts.map +1 -1
- package/dist/store/exodus/on-open.js +26 -1
- package/dist/store/exodus/on-open.js.map +1 -1
- package/dist/store/exodus/prior-recoveries.d.ts +3 -2
- package/dist/store/exodus/prior-recoveries.d.ts.map +1 -1
- package/dist/store/exodus/prior-recoveries.js +7 -3
- package/dist/store/exodus/prior-recoveries.js.map +1 -1
- package/dist/store/exodus/reconcile.d.ts +44 -6
- package/dist/store/exodus/reconcile.d.ts.map +1 -1
- package/dist/store/exodus/reconcile.js +246 -173
- package/dist/store/exodus/reconcile.js.map +1 -1
- package/dist/store/exodus/recovery.d.ts +48 -0
- package/dist/store/exodus/recovery.d.ts.map +1 -1
- package/dist/store/exodus/recovery.js +104 -6
- package/dist/store/exodus/recovery.js.map +1 -1
- package/dist/store/exodus/seal.d.ts.map +1 -1
- package/dist/store/exodus/seal.js +4 -2
- package/dist/store/exodus/seal.js.map +1 -1
- package/dist/store/exodus/table-order.d.ts +6 -0
- package/dist/store/exodus/table-order.d.ts.map +1 -1
- package/dist/store/exodus/table-order.js +15 -0
- package/dist/store/exodus/table-order.js.map +1 -1
- package/dist/store/exodus/types.d.ts +4 -0
- package/dist/store/exodus/types.d.ts.map +1 -1
- package/dist/store/exodus/types.js +4 -0
- package/dist/store/exodus/types.js.map +1 -1
- package/dist/store/exodus/verify-migration.d.ts.map +1 -1
- package/dist/store/exodus/verify-migration.js +22 -17
- package/dist/store/exodus/verify-migration.js.map +1 -1
- package/dist/store/identity-share.d.ts +14 -0
- package/dist/store/identity-share.d.ts.map +1 -1
- package/dist/store/identity-share.js +33 -0
- package/dist/store/identity-share.js.map +1 -1
- package/dist/store/lock.d.ts +33 -0
- package/dist/store/lock.d.ts.map +1 -1
- package/dist/store/lock.js +60 -0
- package/dist/store/lock.js.map +1 -1
- package/dist/store/portable-bundle-import.d.ts.map +1 -1
- package/dist/store/portable-bundle-import.js +7 -0
- package/dist/store/portable-bundle-import.js.map +1 -1
- package/dist/store/portable-bundle-scan.d.ts +3 -2
- package/dist/store/portable-bundle-scan.d.ts.map +1 -1
- package/dist/store/portable-bundle-scan.js +7 -5
- package/dist/store/portable-bundle-scan.js.map +1 -1
- package/dist/store/portable-bundle.d.ts +1 -1
- package/dist/store/portable-bundle.d.ts.map +1 -1
- package/dist/store/portable-bundle.js +9 -1
- package/dist/store/portable-bundle.js.map +1 -1
- package/dist/store/repair-malformed-dbs.d.ts.map +1 -1
- package/dist/store/repair-malformed-dbs.js +36 -6
- package/dist/store/repair-malformed-dbs.js.map +1 -1
- package/dist/store/restore-marker.d.ts +20 -2
- package/dist/store/restore-marker.d.ts.map +1 -1
- package/dist/store/restore-marker.js +41 -1
- package/dist/store/restore-marker.js.map +1 -1
- package/dist/store/row-identity-flag.d.ts +9 -6
- package/dist/store/row-identity-flag.d.ts.map +1 -1
- package/dist/store/row-identity-flag.js +10 -7
- package/dist/store/row-identity-flag.js.map +1 -1
- package/dist/store/row-identity-registry.d.ts +4 -3
- package/dist/store/row-identity-registry.d.ts.map +1 -1
- package/dist/store/row-identity-registry.js +132 -31
- package/dist/store/row-identity-registry.js.map +1 -1
- package/dist/store/row-identity.d.ts +81 -14
- package/dist/store/row-identity.d.ts.map +1 -1
- package/dist/store/row-identity.js +238 -32
- package/dist/store/row-identity.js.map +1 -1
- package/dist/store/schema/cleo-shared/brain.d.ts +368 -0
- package/dist/store/schema/cleo-shared/brain.d.ts.map +1 -1
- package/dist/store/schema/cleo-shared/brain.js +104 -2
- package/dist/store/schema/cleo-shared/brain.js.map +1 -1
- package/dist/store/sync/apply/applier.d.ts +5 -0
- package/dist/store/sync/apply/applier.d.ts.map +1 -1
- package/dist/store/sync/apply/applier.js +5 -1
- package/dist/store/sync/apply/applier.js.map +1 -1
- package/dist/store/sync/apply/parent-delete.d.ts.map +1 -1
- package/dist/store/sync/apply/parent-delete.js +4 -0
- package/dist/store/sync/apply/parent-delete.js.map +1 -1
- package/dist/store/sync/capture.d.ts +13 -1
- package/dist/store/sync/capture.d.ts.map +1 -1
- package/dist/store/sync/capture.js +31 -3
- package/dist/store/sync/capture.js.map +1 -1
- package/dist/store/sync/flags.d.ts +14 -5
- package/dist/store/sync/flags.d.ts.map +1 -1
- package/dist/store/sync/flags.js +20 -8
- package/dist/store/sync/flags.js.map +1 -1
- package/dist/store/sync/genesis.d.ts +130 -0
- package/dist/store/sync/genesis.d.ts.map +1 -0
- package/dist/store/sync/genesis.js +337 -0
- package/dist/store/sync/genesis.js.map +1 -0
- package/dist/store/sync/repair.d.ts +2 -2
- package/dist/store/sync/repair.d.ts.map +1 -1
- package/dist/store/sync/repair.js +55 -15
- package/dist/store/sync/repair.js.map +1 -1
- package/dist/store/sync/sealer.d.ts +14 -0
- package/dist/store/sync/sealer.d.ts.map +1 -1
- package/dist/store/sync/sealer.js +27 -6
- package/dist/store/sync/sealer.js.map +1 -1
- package/dist/store/sync/segments.d.ts +124 -0
- package/dist/store/sync/segments.d.ts.map +1 -0
- package/dist/store/sync/segments.js +267 -0
- package/dist/store/sync/segments.js.map +1 -0
- package/dist/store/sync/sequencing.d.ts +44 -1
- package/dist/store/sync/sequencing.d.ts.map +1 -1
- package/dist/store/sync/sequencing.js +59 -2
- package/dist/store/sync/sequencing.js.map +1 -1
- package/dist/store/sync/timestamp-canon.d.ts +6 -3
- package/dist/store/sync/timestamp-canon.d.ts.map +1 -1
- package/dist/store/sync/timestamp-canon.js +55 -4
- package/dist/store/sync/timestamp-canon.js.map +1 -1
- package/dist/store/sync/txn-signing.d.ts +69 -0
- package/dist/store/sync/txn-signing.d.ts.map +1 -0
- package/dist/store/sync/txn-signing.js +81 -0
- package/dist/store/sync/txn-signing.js.map +1 -0
- package/dist/store/table-classification.d.ts.map +1 -1
- package/dist/store/table-classification.js +30 -0
- package/dist/store/table-classification.js.map +1 -1
- package/dist/store/twin-collapse.d.ts.map +1 -1
- package/dist/store/twin-collapse.js +65 -2
- package/dist/store/twin-collapse.js.map +1 -1
- package/dist/system/backup.d.ts +3 -2
- package/dist/system/backup.d.ts.map +1 -1
- package/dist/system/backup.js +43 -17
- package/dist/system/backup.js.map +1 -1
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +2 -1
- package/dist/system/health.js.map +1 -1
- package/dist/tasks/affected-packages.d.ts +26 -0
- package/dist/tasks/affected-packages.d.ts.map +1 -1
- package/dist/tasks/affected-packages.js +95 -28
- package/dist/tasks/affected-packages.js.map +1 -1
- package/dist/tasks/change-set.d.ts.map +1 -1
- package/dist/tasks/change-set.js +14 -4
- package/dist/tasks/change-set.js.map +1 -1
- package/dist/tasks/done-plan.d.ts.map +1 -1
- package/dist/tasks/done-plan.js +50 -4
- package/dist/tasks/done-plan.js.map +1 -1
- package/dist/tasks/test-run-binding.d.ts +14 -0
- package/dist/tasks/test-run-binding.d.ts.map +1 -1
- package/dist/tasks/test-run-binding.js +35 -4
- package/dist/tasks/test-run-binding.js.map +1 -1
- package/dist/upgrade.d.ts +6 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +58 -11
- package/dist/upgrade.js.map +1 -1
- package/migrations/drizzle-cleo-global/20261010120000_t12894-brain-row-uids/migration.sql +88 -0
- package/migrations/drizzle-cleo-project/20261010120000_t12894-brain-row-uids/migration.sql +92 -0
- package/migrations/sync-journal/20261007120000_t12343-segment-outbox/migration.sql +38 -0
- package/package.json +13 -13
- package/templates/CLEO-INJECTION.md +1 -1
- package/templates/CLEO-REFERENCE.md +2 -2
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
* @see ./migrate.ts — the single copy engine
|
|
35
35
|
* @see ../../doctor/superseded-store.ts — the survey that points here
|
|
36
36
|
*/
|
|
37
|
-
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
37
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
38
38
|
import { tmpdir } from 'node:os';
|
|
39
|
-
import { basename, join } from 'node:path';
|
|
39
|
+
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
40
40
|
import { getLogger } from '../../logger.js';
|
|
41
41
|
import { resolveCleoDir } from '../../paths.js';
|
|
42
42
|
import { resolveDualScopeDbPath } from '../dual-scope-db.js';
|
|
43
43
|
import { lockCompromiseTracker, withLock } from '../lock.js';
|
|
44
44
|
import { openCleoDbSnapshot } from '../open-cleo-db.js';
|
|
45
45
|
import { rowIdentityColumns } from '../row-identity-registry.js';
|
|
46
|
-
import { bareTableDigest, recordBareAccounts } from '../sync/flags.js';
|
|
46
|
+
import { bareTableDigest, forgetBareAccounts, recordBareAccounts } from '../sync/flags.js';
|
|
47
47
|
import { EXODUS_LOCK_STALE_MS, exodusRunLockPath, whileExodusRunHeld } from './abort-events.js';
|
|
48
48
|
import { legacyRowProjection } from './column-transforms.js';
|
|
49
49
|
import { runExodusMigrate } from './migrate.js';
|
|
@@ -51,28 +51,14 @@ import { buildExodusPlan } from './plan.js';
|
|
|
51
51
|
import { rollbackExodusReceipts } from './recovery.js';
|
|
52
52
|
import { buildRuntimeTargetResolver } from './runtime-targets.js';
|
|
53
53
|
import { resolveConsolidatedTableName, resolveTableTargetScope } from './table-name-map.js';
|
|
54
|
-
import { orderTablesForCopy } from './table-order.js';
|
|
54
|
+
import { countRows, hasTable, ident, orderTablesForCopy } from './table-order.js';
|
|
55
55
|
import { describeRemaps, describeUndecided, remapCollidingTaskIds, unlandedRemaps, } from './task-id-remap.js';
|
|
56
|
+
import { BARE_SOURCE_NAME, BARE_STRANDS_SOURCE_NAME } from './types.js';
|
|
56
57
|
const log = getLogger('exodus-reconcile');
|
|
57
58
|
/** Prefix of a reconcile staging dir — deliberately NOT `exodus-staging-`, which on-open resumes. */
|
|
58
59
|
const RECONCILE_DIR_PREFIX = 'exodus-reconcile-';
|
|
59
60
|
/** Receipt filename written inside the reconcile staging dir. */
|
|
60
61
|
const RECEIPT_FILENAME = 'reconcile-receipt.json';
|
|
61
|
-
/** Quote an SQLite identifier. */
|
|
62
|
-
function ident(name) {
|
|
63
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
64
|
-
}
|
|
65
|
-
/** Whether `schema.table` exists on `db`. */
|
|
66
|
-
function hasTable(db, schema, table) {
|
|
67
|
-
return (db
|
|
68
|
-
.prepare(`SELECT 1 AS ok FROM ${ident(schema)}.sqlite_master WHERE type='table' AND name=?`)
|
|
69
|
-
.get(table) !== undefined);
|
|
70
|
-
}
|
|
71
|
-
/** Row count of `schema.table`. */
|
|
72
|
-
function countRows(db, schema, table) {
|
|
73
|
-
const row = db.prepare(`SELECT COUNT(*) AS n FROM ${ident(schema)}.${ident(table)}`).get();
|
|
74
|
-
return Number(row?.n ?? 0);
|
|
75
|
-
}
|
|
76
62
|
/**
|
|
77
63
|
* Count rows of the attached legacy table whose primary key is absent from the
|
|
78
64
|
* live table, or `null` when the tables share no complete primary key.
|
|
@@ -189,95 +175,6 @@ export function legacySourcesHoldRows(sources) {
|
|
|
189
175
|
}
|
|
190
176
|
return false;
|
|
191
177
|
}
|
|
192
|
-
/**
|
|
193
|
-
* Whether a bare legacy table in the live `cleo.db` is one the runtime NO LONGER
|
|
194
|
-
* reads — i.e. its runtime target is a different (prefixed) table. Derived from
|
|
195
|
-
* the runtime's own table bindings via `resolveTarget`, never a hand-kept list:
|
|
196
|
-
* `tasks` → `tasks_tasks` is dead-bare; `audit_log` → `audit_log` is live.
|
|
197
|
-
*
|
|
198
|
-
* @task T12346
|
|
199
|
-
*/
|
|
200
|
-
function isRuntimeDeadBare(resolveTarget, table) {
|
|
201
|
-
const target = resolveTarget('tasks', table);
|
|
202
|
-
return target.kind !== 'skip' && target.targetName !== table;
|
|
203
|
-
}
|
|
204
|
-
/** Logical source name for the live store's bare task-core family. */
|
|
205
|
-
const BARE_SOURCE_NAME = 'tasks (cleo.db bare task-core)';
|
|
206
|
-
/**
|
|
207
|
-
* The live store's own dead bare task-core family as a reconcile source — item
|
|
208
|
-
* 2 of T12319, found real by the T12346 sweep: a stranded `cleo.db` can hold
|
|
209
|
-
* rows in bare tables the runtime no longer reads that exist in NO legacy file
|
|
210
|
-
* (llmtxt: 1,972 `task_labels`; versionguard: 20 `task_dependencies`).
|
|
211
|
-
*
|
|
212
|
-
* Offered ONLY while the prefixed `tasks_tasks` is still empty — i.e. the
|
|
213
|
-
* project never ran on the consolidated store. Once it has, a bare row missing
|
|
214
|
-
* from the prefixed family may be one the runtime deliberately removed, and
|
|
215
|
-
* re-copying it would resurrect it.
|
|
216
|
-
*
|
|
217
|
-
* Materialised as a standalone file in `outDir` holding exactly the qualifying
|
|
218
|
-
* rows, built table by table from the live file's own DDL (a whole-file copy
|
|
219
|
-
* cannot be pruned: sqlite-vec `vec0` tables cannot be dropped without their
|
|
220
|
-
* module). Rows a FRESH project store already holds (e.g. the lineage's 18
|
|
221
|
-
* backfilled `commits`) are not project data and are excluded — measured by
|
|
222
|
-
* building such a store, not by guessing — so a reconciled project ends up
|
|
223
|
-
* with exactly what a fresh one has there, the same as exodus-on-open.
|
|
224
|
-
* The table set is fixed when the file is built, so the post-copy verification
|
|
225
|
-
* judges exactly what was copied.
|
|
226
|
-
*
|
|
227
|
-
* @returns The descriptor (path = the materialised file), or `null`.
|
|
228
|
-
*/
|
|
229
|
-
async function bareTaskCoreSource(liveStorePath, resolveTarget, outDir) {
|
|
230
|
-
const live = openCleoDbSnapshot(liveStorePath, { readOnly: true });
|
|
231
|
-
let tables;
|
|
232
|
-
try {
|
|
233
|
-
if (hasTable(live.db, 'main', 'tasks_tasks') && countRows(live.db, 'main', 'tasks_tasks') > 0)
|
|
234
|
-
return null;
|
|
235
|
-
tables = live.db
|
|
236
|
-
.prepare("SELECT name, sql FROM main.sqlite_master WHERE type='table' AND sql IS NOT NULL")
|
|
237
|
-
.all().filter((t) => isRuntimeDeadBare(resolveTarget, t.name) && countRows(live.db, 'main', t.name) > 0);
|
|
238
|
-
}
|
|
239
|
-
finally {
|
|
240
|
-
live.close();
|
|
241
|
-
}
|
|
242
|
-
if (tables.length === 0)
|
|
243
|
-
return null;
|
|
244
|
-
const seedPath = await buildFreshProjectStore(join(outDir, 'fresh-project-seed.db'));
|
|
245
|
-
const path = join(outDir, 'cleo-bare-task-core.db');
|
|
246
|
-
const snap = openCleoDbSnapshot(path, { readOnly: false });
|
|
247
|
-
let kept = 0;
|
|
248
|
-
try {
|
|
249
|
-
snap.db.exec('PRAGMA foreign_keys=OFF');
|
|
250
|
-
snap.db.exec(`ATTACH DATABASE '${liveStorePath.replace(/'/g, "''")}' AS live`);
|
|
251
|
-
snap.db.exec(`ATTACH DATABASE '${seedPath.replace(/'/g, "''")}' AS seed`);
|
|
252
|
-
for (const t of tables) {
|
|
253
|
-
snap.db.exec(t.sql);
|
|
254
|
-
snap.db.exec(`INSERT INTO main.${ident(t.name)} SELECT * FROM live.${ident(t.name)}`);
|
|
255
|
-
if (hasTable(snap.db, 'seed', t.name)) {
|
|
256
|
-
const seedCols = new Set(snap.db.prepare(`PRAGMA seed.table_info(${ident(t.name)})`).all().map((c) => c.name));
|
|
257
|
-
const cols = snap.db.prepare(`PRAGMA main.table_info(${ident(t.name)})`).all();
|
|
258
|
-
// A seed row is identified by its KEY: seeded rows carry the time they
|
|
259
|
-
// were seeded (e.g. `commits.created_at`), so whole-row equality never
|
|
260
|
-
// matches a seed written on another day. Keyless tables compare rows.
|
|
261
|
-
const pk = cols.filter((c) => c.pk > 0 && seedCols.has(c.name)).map((c) => c.name);
|
|
262
|
-
const match = (pk.length > 0 ? pk : cols.map((c) => c.name).filter((c) => seedCols.has(c)))
|
|
263
|
-
.map((c) => `s.${ident(c)} IS m.${ident(c)}`)
|
|
264
|
-
.join(' AND ');
|
|
265
|
-
if (match.length > 0)
|
|
266
|
-
snap.db.exec(`DELETE FROM main.${ident(t.name)} AS m WHERE EXISTS (SELECT 1 FROM seed.${ident(t.name)} s WHERE ${match})`);
|
|
267
|
-
}
|
|
268
|
-
const n = countRows(snap.db, 'main', t.name);
|
|
269
|
-
if (n === 0)
|
|
270
|
-
snap.db.exec(`DROP TABLE main.${ident(t.name)}`);
|
|
271
|
-
kept += n;
|
|
272
|
-
}
|
|
273
|
-
snap.db.exec('DETACH DATABASE live');
|
|
274
|
-
snap.db.exec('DETACH DATABASE seed');
|
|
275
|
-
}
|
|
276
|
-
finally {
|
|
277
|
-
snap.close();
|
|
278
|
-
}
|
|
279
|
-
return kept > 0 ? { name: BARE_SOURCE_NAME, path, targetScope: 'project' } : null;
|
|
280
|
-
}
|
|
281
178
|
/**
|
|
282
179
|
* Key digests ({@link bareTableDigest}) of the live store's bare tables that
|
|
283
180
|
* the materialised bare source at `barePath` carries (T13319).
|
|
@@ -301,26 +198,6 @@ function bareAccounts(liveStorePath, barePath) {
|
|
|
301
198
|
live.close();
|
|
302
199
|
}
|
|
303
200
|
}
|
|
304
|
-
/**
|
|
305
|
-
* Build a FRESH project store — consolidated schema plus the tasks-domain
|
|
306
|
-
* lineage, exactly as a brand-new project gets it — and return its path. Its
|
|
307
|
-
* rows are, by construction, only what CLEO seeds (e.g. the 18 backfilled
|
|
308
|
-
* `commits`), never project data.
|
|
309
|
-
*/
|
|
310
|
-
async function buildFreshProjectStore(path) {
|
|
311
|
-
const { getDualScopeNativeDb, openDualScopeDbAtPath } = await import('../dual-scope-db.js');
|
|
312
|
-
const { ensureTasksDomainTables, seedTasksMeta } = await import('../sqlite.js');
|
|
313
|
-
const handle = await openDualScopeDbAtPath('project', path, undefined, { dedicated: true });
|
|
314
|
-
try {
|
|
315
|
-
const native = getDualScopeNativeDb(handle);
|
|
316
|
-
ensureTasksDomainTables(native, path);
|
|
317
|
-
seedTasksMeta(native);
|
|
318
|
-
}
|
|
319
|
-
finally {
|
|
320
|
-
handle.close();
|
|
321
|
-
}
|
|
322
|
-
return path;
|
|
323
|
-
}
|
|
324
201
|
/**
|
|
325
202
|
* From the bare task-core snapshot, extract the rows that are strictly NEWER
|
|
326
203
|
* (`updated_at`) than the same-key row in the legacy `tasks.db`, into their own
|
|
@@ -421,7 +298,11 @@ async function revertReconcile(liveStorePath, stagingDir) {
|
|
|
421
298
|
dedicated: true,
|
|
422
299
|
});
|
|
423
300
|
try {
|
|
424
|
-
|
|
301
|
+
const native = getDualScopeNativeDb(handle);
|
|
302
|
+
const reverted = rollbackExodusReceipts(native, stagingDir);
|
|
303
|
+
// A reverted run carried nothing, so its record of carried tables goes too.
|
|
304
|
+
forgetBareAccounts(native, basename(stagingDir));
|
|
305
|
+
return reverted;
|
|
425
306
|
}
|
|
426
307
|
finally {
|
|
427
308
|
handle.close();
|
|
@@ -571,6 +452,7 @@ function alteredLiveTables(liveStorePath, beforePath, counts, sequenceSeed) {
|
|
|
571
452
|
* @task T12355
|
|
572
453
|
*/
|
|
573
454
|
export async function unmigratedBareSources(liveStorePath, resolveTarget, legacyTasksPath, outDir) {
|
|
455
|
+
const { bareTaskCoreSource } = await import('./bare-family.js');
|
|
574
456
|
const bare = await bareTaskCoreSource(liveStorePath, resolveTarget, outDir);
|
|
575
457
|
if (bare === null)
|
|
576
458
|
return { first: [], last: [] };
|
|
@@ -582,11 +464,125 @@ export async function unmigratedBareSources(liveStorePath, resolveTarget, legacy
|
|
|
582
464
|
last: [bare],
|
|
583
465
|
};
|
|
584
466
|
}
|
|
467
|
+
/** The receipt name a rolled-back run's receipt is renamed to (T13309). */
|
|
468
|
+
const ROLLED_BACK_RECEIPT_FILENAME = 'reconcile-receipt.rolled-back.json';
|
|
469
|
+
/**
|
|
470
|
+
* Revert a reconciled run from its receipt (T13309): every row it inserted is
|
|
471
|
+
* removed, but only when each is still exactly as the run wrote it (the
|
|
472
|
+
* exodus receipts guard refuses the whole revert otherwise). Holds the store's
|
|
473
|
+
* exodus lock, so no reconcile or exodus-on-open runs alongside. The receipt
|
|
474
|
+
* is renamed, so neither a later run's earlier-recoveries scan nor the sync
|
|
475
|
+
* check for stranded bare rows trusts it again.
|
|
476
|
+
*
|
|
477
|
+
* @param projectRoot - Absolute project root.
|
|
478
|
+
* @param runDir - The run directory (`.cleo/exodus-reconcile-<iso>`), absolute
|
|
479
|
+
* or relative to the project's `.cleo`.
|
|
480
|
+
* @throws When `runDir` is not a reconciled run of this project, or a row the
|
|
481
|
+
* run inserted has changed since.
|
|
482
|
+
* @example
|
|
483
|
+
* ```ts
|
|
484
|
+
* await rollbackSupersededReconcile('/p', 'exodus-reconcile-20261007T020000Z');
|
|
485
|
+
* ```
|
|
486
|
+
*/
|
|
487
|
+
export async function rollbackSupersededReconcile(projectRoot, runDir) {
|
|
488
|
+
const cleoDir = resolveCleoDir(projectRoot);
|
|
489
|
+
const dir = isAbsolute(runDir) ? runDir : join(cleoDir, runDir);
|
|
490
|
+
const receiptPath = join(dir, RECEIPT_FILENAME);
|
|
491
|
+
if (dirname(dir) !== cleoDir || !basename(dir).startsWith(RECONCILE_DIR_PREFIX)) {
|
|
492
|
+
// @sync-invariant none:local-only rolling back a local reconcile run is refused before any write; never replicated
|
|
493
|
+
throw new Error(`${runDir} is not a reconcile run directory under ${cleoDir}`);
|
|
494
|
+
}
|
|
495
|
+
let receipt;
|
|
496
|
+
try {
|
|
497
|
+
receipt = JSON.parse(readFileSync(receiptPath, 'utf8'));
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
// @sync-invariant none:local-only rolling back a local reconcile run is refused before any write; never replicated
|
|
501
|
+
throw new Error(`no reconcile receipt at ${receiptPath}`);
|
|
502
|
+
}
|
|
503
|
+
const fields = new Map(typeof receipt === 'object' && receipt !== null ? Object.entries(receipt) : []);
|
|
504
|
+
const outcome = fields.get('outcome');
|
|
505
|
+
// A refused run whose own revert failed left rows behind (T13384).
|
|
506
|
+
const unreverted = outcome === 'refused' &&
|
|
507
|
+
Number(fields.get('rowsCopied') ?? 0) > Number(fields.get('rolledBack') ?? 0);
|
|
508
|
+
if (outcome !== 'reconciled' && !unreverted) {
|
|
509
|
+
// @sync-invariant none:local-only rolling back a local reconcile run is refused before any write; never replicated
|
|
510
|
+
throw new Error(`${receiptPath} records outcome ${String(outcome)}; only a reconciled run, or a refused run whose revert failed, can be rolled back`);
|
|
511
|
+
}
|
|
512
|
+
const liveStorePath = resolveDualScopeDbPath('project', projectRoot);
|
|
513
|
+
const lock = lockCompromiseTracker();
|
|
514
|
+
const lockPath = exodusRunLockPath(liveStorePath);
|
|
515
|
+
const rowsReverted = await withLock(lockPath, whileExodusRunHeld(lockPath, async () => revertReconcile(liveStorePath, dir)), { stale: EXODUS_LOCK_STALE_MS, retries: 30, onCompromised: lock.onCompromised });
|
|
516
|
+
const rolledBack = join(dir, ROLLED_BACK_RECEIPT_FILENAME);
|
|
517
|
+
renameSync(receiptPath, rolledBack);
|
|
518
|
+
log.info({ runDir: dir, rowsReverted }, 'exodus-reconcile: rolled back');
|
|
519
|
+
return { runDir: dir, rowsReverted, receiptPath: rolledBack };
|
|
520
|
+
}
|
|
521
|
+
/** Create a fresh run directory under `.cleo/` for a reconcile apply. */
|
|
522
|
+
function newRunDir(cleoDir) {
|
|
523
|
+
const iso = new Date()
|
|
524
|
+
.toISOString()
|
|
525
|
+
.replace(/[:]/g, '')
|
|
526
|
+
.replace(/\..+Z$/, 'Z');
|
|
527
|
+
const dir = join(cleoDir, `${RECONCILE_DIR_PREFIX}${iso}`);
|
|
528
|
+
mkdirSync(dir, { recursive: true });
|
|
529
|
+
return dir;
|
|
530
|
+
}
|
|
531
|
+
/** Write `result`'s receipt into its run directory and return it with the path. */
|
|
532
|
+
function writeReceipt(result, stagingDir) {
|
|
533
|
+
const receiptPath = join(stagingDir, RECEIPT_FILENAME);
|
|
534
|
+
const receipt = { ...result, receiptPath };
|
|
535
|
+
writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, 'utf8');
|
|
536
|
+
log.info({ outcome: receipt.outcome, rowsCopied: receipt.rowsCopied, receiptPath }, `exodus-reconcile: ${receipt.reason}`);
|
|
537
|
+
return receipt;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* A bare-strands run with nothing to copy — every stranded row is present, or
|
|
541
|
+
* deliberately skipped: record in the store that every bare table is settled
|
|
542
|
+
* (T13320), so the sync refusal for stranded rows stands down, and a
|
|
543
|
+
* `reconciled` receipt naming any skipped rows (T13309). This is also how a
|
|
544
|
+
* store reconciled before T13319 gets its record. A dry run only reports.
|
|
545
|
+
* A stranded row neither present nor listed as skipped refuses the settle,
|
|
546
|
+
* so the refusal stands (T13333).
|
|
547
|
+
*/
|
|
548
|
+
async function settleStrands(none, strands, cleoDir, resolveTarget) {
|
|
549
|
+
if (none.dryRun || strands.accounted.length === 0)
|
|
550
|
+
return none;
|
|
551
|
+
const unaccounted = unaccountedStrands(none.liveStorePath, strands, resolveTarget);
|
|
552
|
+
if (unaccounted.length > 0) {
|
|
553
|
+
return {
|
|
554
|
+
...none,
|
|
555
|
+
outcome: 'refused',
|
|
556
|
+
reason: `stranded rows neither in cleo.db nor listed as skipped: ${describeGaps(unaccounted)}`,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const stagingDir = newRunDir(cleoDir);
|
|
560
|
+
await recordCarriedBareTables(none.liveStorePath, strands.accounted, basename(stagingDir));
|
|
561
|
+
return writeReceipt({
|
|
562
|
+
...none,
|
|
563
|
+
outcome: 'reconciled',
|
|
564
|
+
stagingDir,
|
|
565
|
+
accounted: strands.accounted,
|
|
566
|
+
snapshotPath: null,
|
|
567
|
+
reason: `nothing copied; ${none.reason}`,
|
|
568
|
+
}, stagingDir);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* The tables where a bare-strands run's accounting copy (every stranded row
|
|
572
|
+
* it did not list as skipped, renumbered) holds rows the live store lacks by
|
|
573
|
+
* key (T13333). Empty when every such row is in the live store.
|
|
574
|
+
*/
|
|
575
|
+
function unaccountedStrands(liveStorePath, strands, resolveTarget) {
|
|
576
|
+
if (strands.accountingPath === null)
|
|
577
|
+
return [];
|
|
578
|
+
return assessSupersededProjectStores(liveStorePath, [{ name: BARE_STRANDS_SOURCE_NAME, path: strands.accountingPath, targetScope: 'project' }], resolveTarget).filter((c) => !isComplete([c]));
|
|
579
|
+
}
|
|
585
580
|
/**
|
|
586
581
|
* Reconcile a project's stranded legacy stores into its live `cleo.db`.
|
|
587
582
|
*
|
|
588
583
|
* @param projectRoot - Absolute project root.
|
|
589
|
-
* @param options - `dryRun` assesses and reports without writing anything
|
|
584
|
+
* @param options - `dryRun` assesses and reports without writing anything;
|
|
585
|
+
* `bareStrands` copies the live store's stranded bare rows (T13309).
|
|
590
586
|
* @returns The receipt; `outcome: 'refused'` means the copy did not verify and
|
|
591
587
|
* every row it inserted was reverted.
|
|
592
588
|
* @example
|
|
@@ -607,7 +603,7 @@ export async function reconcileSupersededStores(projectRoot, options = {}) {
|
|
|
607
603
|
/** {@link reconcileSupersededStores} with exodus-on-open suppressed for the store. */
|
|
608
604
|
async function reconcileSuppressed(projectRoot, options, liveStorePath) {
|
|
609
605
|
const dryRun = options.dryRun === true;
|
|
610
|
-
const
|
|
606
|
+
const mode = options.bareStrands === true ? 'bare-strands' : options.additive === true ? 'additive' : 'full';
|
|
611
607
|
const plan = buildExodusPlan(projectRoot);
|
|
612
608
|
// Rows land where the RUNTIME reads them (T12346), derived from its bindings.
|
|
613
609
|
const resolveTarget = await buildRuntimeTargetResolver();
|
|
@@ -615,29 +611,69 @@ async function reconcileSuppressed(projectRoot, options, liveStorePath) {
|
|
|
615
611
|
// so a dry-run writes nothing there (an apply copies it into its staging dir).
|
|
616
612
|
const scratch = mkdtempSync(join(tmpdir(), 'cleo-reconcile-'));
|
|
617
613
|
try {
|
|
618
|
-
return await reconcileWithScratch(projectRoot, dryRun,
|
|
614
|
+
return await reconcileWithScratch(projectRoot, dryRun, mode, liveStorePath, plan, resolveTarget, scratch);
|
|
619
615
|
}
|
|
620
616
|
finally {
|
|
621
617
|
rmSync(scratch, { recursive: true, force: true });
|
|
622
618
|
}
|
|
623
619
|
}
|
|
624
620
|
/** The body of {@link reconcileSupersededStores}, with a scratch dir it owns. */
|
|
625
|
-
async function reconcileWithScratch(projectRoot, dryRun,
|
|
621
|
+
async function reconcileWithScratch(projectRoot, dryRun, mode, liveStorePath, plan, resolveTarget, scratch) {
|
|
622
|
+
const additive = mode === 'additive';
|
|
623
|
+
const strandsMode = mode === 'bare-strands';
|
|
624
|
+
const cleoDir = resolveCleoDir(projectRoot);
|
|
626
625
|
const legacyFiles = plan.sources.filter((s) => s.targetScope === 'project' && existsSync(s.path));
|
|
626
|
+
// Bare-strands mode (T13309) reads only the live store's stranded bare rows,
|
|
627
|
+
// and only once the legacy files are carried: a full run recovers a legacy
|
|
628
|
+
// task whose id was reused, and this run then finds that recovery in the
|
|
629
|
+
// full run's receipt instead of recovering the same task a second time.
|
|
630
|
+
if (strandsMode && legacyFiles.length > 0 && existsSync(liveStorePath)) {
|
|
631
|
+
const pending = assessSupersededProjectStores(liveStorePath, legacyFiles, resolveTarget);
|
|
632
|
+
if (!isComplete(pending)) {
|
|
633
|
+
return {
|
|
634
|
+
outcome: 'nothing-to-reconcile',
|
|
635
|
+
mode,
|
|
636
|
+
conflicts: [],
|
|
637
|
+
remaps: [],
|
|
638
|
+
dryRun,
|
|
639
|
+
projectRoot,
|
|
640
|
+
liveStorePath,
|
|
641
|
+
sourcePaths: legacyFiles.map((s) => s.path),
|
|
642
|
+
before: pending,
|
|
643
|
+
after: [],
|
|
644
|
+
rowsCopied: 0,
|
|
645
|
+
rolledBack: 0,
|
|
646
|
+
stagingDir: null,
|
|
647
|
+
receiptPath: null,
|
|
648
|
+
reason: `legacy files still hold rows missing from cleo.db (${describeGaps(pending)}); run ` +
|
|
649
|
+
'`cleo doctor superseded-store --reconcile` first, then the bare-strands reconcile',
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const strands = strandsMode && existsSync(liveStorePath)
|
|
654
|
+
? await (await import('./bare-family.js')).bareStrandSource(liveStorePath, resolveTarget, scratch, cleoDir)
|
|
655
|
+
: null;
|
|
656
|
+
// Rows bare-strands mode deliberately leaves uncopied, named in the receipt.
|
|
657
|
+
const skipped = strands?.skipped ?? [];
|
|
658
|
+
// What the run reads: the legacy files, or the stranded bare rows.
|
|
659
|
+
const originals = strandsMode ? (strands?.source ? [strands.source] : []) : legacyFiles;
|
|
627
660
|
// T13172: a legacy task whose id a DIFFERENT live task holds is renumbered in
|
|
628
661
|
// a scratch copy (references re-pointed) and the run reads that copy, so it
|
|
629
662
|
// is recovered, never skipped by INSERT OR IGNORE and counted as present.
|
|
630
663
|
// Additive runs never write the task graph, so they never renumber.
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
664
|
+
// Bare-strands mode renumbered before judging its rows (T13333).
|
|
665
|
+
const remap = strandsMode
|
|
666
|
+
? (strands?.remap ?? { sources: [], remaps: [], undecided: null, remappedPath: null })
|
|
667
|
+
: !additive && existsSync(liveStorePath)
|
|
668
|
+
? remapCollidingTaskIds(liveStorePath, legacyFiles, scratch, cleoDir)
|
|
669
|
+
: { sources: legacyFiles, remaps: [], undecided: null, remappedPath: null };
|
|
634
670
|
// An undecided collision withholds the WHOLE task graph (review MED-2): its
|
|
635
671
|
// children, dependencies and criteria would otherwise attach to the live
|
|
636
672
|
// task that holds its id. History tables still copy, and from the ORIGINAL
|
|
637
673
|
// legacy files: the renumbered copy would point history rows at fresh ids
|
|
638
674
|
// no live task holds, which the next `cleo add` could then mint (review LOW).
|
|
639
675
|
const graphWithheld = !additive && remap.undecided !== null;
|
|
640
|
-
const fileSources = graphWithheld ?
|
|
676
|
+
const fileSources = graphWithheld ? originals : remap.sources;
|
|
641
677
|
// The receipt records each remap with the legacy creation time and type,
|
|
642
678
|
// which later runs check before trusting it (T13183).
|
|
643
679
|
const remaps = remap.remaps;
|
|
@@ -648,8 +684,8 @@ async function reconcileWithScratch(projectRoot, dryRun, additive, liveStorePath
|
|
|
648
684
|
(undecided ? `; ${describeUndecided(undecided)}` : '');
|
|
649
685
|
// An additive run is for a project already live on the consolidated store;
|
|
650
686
|
// its bare family is not a source (and bareTaskCoreSource agrees).
|
|
651
|
-
const bare = !additive && existsSync(liveStorePath)
|
|
652
|
-
? await bareTaskCoreSource(liveStorePath, resolveTarget, scratch)
|
|
687
|
+
const bare = !additive && !strandsMode && existsSync(liveStorePath)
|
|
688
|
+
? await (await import('./bare-family.js')).bareTaskCoreSource(liveStorePath, resolveTarget, scratch)
|
|
653
689
|
: null;
|
|
654
690
|
// The bare tables this run carries, by key digest. A verified run records
|
|
655
691
|
// them in the store (T13320) — the sync refusal for stranded bare rows
|
|
@@ -675,15 +711,17 @@ async function reconcileWithScratch(projectRoot, dryRun, additive, liveStorePath
|
|
|
675
711
|
// Legacy FILES first so they win any key both hold; the bare family fills gaps.
|
|
676
712
|
const sources = bare ? [...fileSources, bare] : fileSources;
|
|
677
713
|
const base = {
|
|
678
|
-
mode
|
|
679
|
-
conflicts: (undecided ? [undecided] : []),
|
|
714
|
+
mode,
|
|
715
|
+
conflicts: [...(undecided ? [undecided] : []), ...skipped],
|
|
680
716
|
// A withheld graph copies no task, so no remap is applied.
|
|
681
717
|
remaps: graphWithheld ? [] : remaps,
|
|
682
718
|
dryRun,
|
|
683
719
|
projectRoot,
|
|
684
720
|
liveStorePath,
|
|
685
721
|
// The files read: the legacy originals, never the renumbered scratch copy.
|
|
686
|
-
sourcePaths:
|
|
722
|
+
sourcePaths: strandsMode
|
|
723
|
+
? [liveStorePath]
|
|
724
|
+
: [...legacyFiles, ...(bare ? [bare] : [])].map((s) => s.path),
|
|
687
725
|
after: [],
|
|
688
726
|
rowsCopied: 0,
|
|
689
727
|
rolledBack: 0,
|
|
@@ -691,14 +729,19 @@ async function reconcileWithScratch(projectRoot, dryRun, additive, liveStorePath
|
|
|
691
729
|
receiptPath: null,
|
|
692
730
|
};
|
|
693
731
|
if (sources.length === 0 || !existsSync(liveStorePath)) {
|
|
694
|
-
|
|
732
|
+
const none = {
|
|
695
733
|
...base,
|
|
696
734
|
outcome: 'nothing-to-reconcile',
|
|
697
735
|
before: [],
|
|
698
|
-
reason:
|
|
699
|
-
?
|
|
700
|
-
:
|
|
736
|
+
reason: !existsSync(liveStorePath)
|
|
737
|
+
? `no live store at ${liveStorePath}; the legacy files are still the live data`
|
|
738
|
+
: strandsMode
|
|
739
|
+
? strands?.populated === false
|
|
740
|
+
? 'tasks_tasks is empty: the bare family is copied whole by `cleo doctor superseded-store --reconcile`'
|
|
741
|
+
: `no stranded bare row is left to copy${skipped.length > 0 ? `; ${describeConflicts(skipped)}` : ''}`
|
|
742
|
+
: 'no legacy project store (tasks.db / brain.db / conduit.db, or bare task tables in an unmigrated cleo.db) is present',
|
|
701
743
|
};
|
|
744
|
+
return strands?.populated ? settleStrands(none, strands, cleoDir, resolveTarget) : none;
|
|
702
745
|
}
|
|
703
746
|
const before = assessSupersededProjectStores(liveStorePath, sources, resolveTarget);
|
|
704
747
|
const plannedConflicts = additive ? conflictsOf(before) : [];
|
|
@@ -706,7 +749,7 @@ async function reconcileWithScratch(projectRoot, dryRun, additive, liveStorePath
|
|
|
706
749
|
const withheldOf = (counts) => conflictsOf(counts)
|
|
707
750
|
.filter((c) => c.reason === 'live-authoritative')
|
|
708
751
|
.map((c) => ({ ...c, reason: 'withheld-undecided' }));
|
|
709
|
-
const withheldConflicts = (counts) => undecided ? [undecided, ...withheldOf(counts)] :
|
|
752
|
+
const withheldConflicts = (counts) => undecided ? [undecided, ...withheldOf(counts), ...skipped] : skipped;
|
|
710
753
|
const withheldNote = (counts) => `; the task graph was NOT copied: ${undecided ? describeUndecided(undecided) : ''}. ` +
|
|
711
754
|
`Withheld: ${describeConflicts(withheldOf(counts))}. Correct the legacy created_at of ` +
|
|
712
755
|
`${(undecided?.ids ?? []).join(', ')} to an ISO-8601 time, then run the reconcile again`;
|
|
@@ -735,12 +778,15 @@ async function reconcileWithScratch(projectRoot, dryRun, additive, liveStorePath
|
|
|
735
778
|
};
|
|
736
779
|
}
|
|
737
780
|
if (isComplete(before)) {
|
|
738
|
-
|
|
781
|
+
const none = {
|
|
739
782
|
...base,
|
|
740
783
|
outcome: 'nothing-to-reconcile',
|
|
741
784
|
before,
|
|
742
|
-
reason: `${undecided ? 'every other legacy row' : 'every legacy row'} is already present in cleo.db — nothing to copy${graphWithheld ? (undecided ? `; ${describeUndecided(undecided)}` : '') : remapNote}`,
|
|
785
|
+
reason: `${undecided ? 'every other legacy row' : 'every legacy row'} is already present in cleo.db — nothing to copy${graphWithheld ? (undecided ? `; ${describeUndecided(undecided)}` : '') : remapNote}${skipped.length > 0 ? `; ${describeConflicts(skipped)}` : ''}`,
|
|
743
786
|
};
|
|
787
|
+
return strands?.populated && !graphWithheld
|
|
788
|
+
? settleStrands(none, strands, cleoDir, resolveTarget)
|
|
789
|
+
: none;
|
|
744
790
|
}
|
|
745
791
|
if (dryRun) {
|
|
746
792
|
return {
|
|
@@ -756,19 +802,14 @@ async function reconcileWithScratch(projectRoot, dryRun, additive, liveStorePath
|
|
|
756
802
|
? `would copy the missing rows of the history tables; ${describeConflicts(plannedConflicts.filter((c) => c.reason === 'live-authoritative'))} (key collisions are counted after the copy)`
|
|
757
803
|
: graphWithheld
|
|
758
804
|
? `would copy the missing history rows only${withheldNote(before)}`
|
|
759
|
-
: `would copy the missing rows of: ${describeGaps(before)}${remapNote}`,
|
|
805
|
+
: `would copy the missing rows of: ${describeGaps(before)}${remapNote}${skipped.length > 0 ? `; would leave uncopied: ${describeConflicts(skipped)}` : ''}`,
|
|
760
806
|
};
|
|
761
807
|
}
|
|
762
|
-
const
|
|
763
|
-
.toISOString()
|
|
764
|
-
.replace(/[:]/g, '')
|
|
765
|
-
.replace(/\..+Z$/, 'Z');
|
|
766
|
-
const stagingDir = join(resolveCleoDir(projectRoot), `${RECONCILE_DIR_PREFIX}${iso}`);
|
|
767
|
-
mkdirSync(stagingDir, { recursive: true });
|
|
808
|
+
const stagingDir = newRunDir(cleoDir);
|
|
768
809
|
// Keep the materialised bare source and the renumbered tasks copy with the
|
|
769
810
|
// run's other evidence.
|
|
770
811
|
const copySources = sources.map((s) => {
|
|
771
|
-
if (s !== bare && s.path !== remap.remappedPath)
|
|
812
|
+
if (s !== bare && s.path !== remap.remappedPath && s.name !== BARE_STRANDS_SOURCE_NAME)
|
|
772
813
|
return s;
|
|
773
814
|
const kept = join(stagingDir, basename(s.path));
|
|
774
815
|
copyFileSync(s.path, kept);
|
|
@@ -792,7 +833,8 @@ async function reconcileWithScratch(projectRoot, dryRun, additive, liveStorePath
|
|
|
792
833
|
const result = await withLock(lockPath, whileExodusRunHeld(lockPath, async () => {
|
|
793
834
|
// Prove "never overwrites": every live row that existed before the copy
|
|
794
835
|
// must still exist, byte for byte, afterwards.
|
|
795
|
-
|
|
836
|
+
// Bare-strands mode keeps this snapshot with the receipt (T13309).
|
|
837
|
+
const liveBefore = join(strandsMode ? stagingDir : scratch, 'live-before.db');
|
|
796
838
|
snapshotLive(liveStorePath, liveBefore);
|
|
797
839
|
const copied = await runExodusMigrate(reconcilePlan, false, (msg) => log.debug(msg), {
|
|
798
840
|
projectOnly: true,
|
|
@@ -832,17 +874,29 @@ async function reconcileWithScratch(projectRoot, dryRun, additive, liveStorePath
|
|
|
832
874
|
// allocation and the copy: the copy skipped it, and verification by key
|
|
833
875
|
// would call it present (review LOW-1).
|
|
834
876
|
const unlanded = migrated.ok && !graphWithheld ? unlandedRemaps(liveStorePath, remap.remaps) : [];
|
|
877
|
+
// Bare-strands mode: every stranded row not listed as skipped is now in
|
|
878
|
+
// the live store (T13333), and no copied row refers to a task the live
|
|
879
|
+
// store lacks (T13334).
|
|
880
|
+
const strandsChecked = migrated.ok && strands && !graphWithheld;
|
|
881
|
+
const unaccounted = strandsChecked
|
|
882
|
+
? unaccountedStrands(liveStorePath, strands, resolveTarget)
|
|
883
|
+
: [];
|
|
884
|
+
const dangling = strandsChecked && strands.source
|
|
885
|
+
? (await import('./bare-family.js')).danglingStrandReferences(liveStorePath, strands.source.path)
|
|
886
|
+
: [];
|
|
835
887
|
const verified = migrated.ok &&
|
|
836
888
|
settled &&
|
|
837
889
|
lost.length === 0 &&
|
|
838
890
|
altered.length === 0 &&
|
|
839
|
-
unlanded.length === 0
|
|
891
|
+
unlanded.length === 0 &&
|
|
892
|
+
unaccounted.length === 0 &&
|
|
893
|
+
dangling.length === 0;
|
|
840
894
|
// The store's own record of the bare tables this run carried (T13320),
|
|
841
895
|
// written only once the copy verified, so a revert never leaves one.
|
|
842
896
|
let unrecorded = null;
|
|
843
|
-
if (verified && bare && !graphWithheld) {
|
|
897
|
+
if (verified && (bare || strands) && !graphWithheld) {
|
|
844
898
|
try {
|
|
845
|
-
await recordCarriedBareTables(liveStorePath, accounted, basename(stagingDir));
|
|
899
|
+
await recordCarriedBareTables(liveStorePath, strands ? strands.accounted : accounted, basename(stagingDir));
|
|
846
900
|
}
|
|
847
901
|
catch (error) {
|
|
848
902
|
unrecorded = error instanceof Error ? error.message : String(error);
|
|
@@ -857,15 +911,32 @@ async function reconcileWithScratch(projectRoot, dryRun, additive, liveStorePath
|
|
|
857
911
|
after,
|
|
858
912
|
rowsCopied,
|
|
859
913
|
stagingDir,
|
|
860
|
-
|
|
914
|
+
// A settled run accounts for each bare table it carried (full mode)
|
|
915
|
+
// or settled (bare-strands mode), so the sync refusal for stranded
|
|
916
|
+
// rows stands down for it (T13319, T13309).
|
|
917
|
+
...(!graphWithheld && (strands || bare)
|
|
918
|
+
? { accounted: strands ? strands.accounted : accounted }
|
|
919
|
+
: {}),
|
|
920
|
+
...(strandsMode ? { snapshotPath: liveBefore } : {}),
|
|
861
921
|
reason: additive
|
|
862
922
|
? `copied ${rowsCopied} row(s) with keys absent from live; live rows unchanged; ${describeConflicts(conflicts)}`
|
|
863
923
|
: graphWithheld
|
|
864
924
|
? `copied ${rowsCopied} history row(s)${withheldNote(after)}`
|
|
865
|
-
:
|
|
925
|
+
: strandsMode
|
|
926
|
+
? `copied ${rowsCopied} stranded bare row(s); every other stranded row is accounted for${remapNote}${skipped.length > 0 ? `; ${describeConflicts(skipped)}` : ''}; live snapshot before the copy: ${liveBefore}`
|
|
927
|
+
: `copied ${rowsCopied} row(s); every legacy row is now present in cleo.db${remapNote}`,
|
|
866
928
|
};
|
|
867
929
|
}
|
|
868
|
-
|
|
930
|
+
// A revert that cannot run still leaves a refused receipt naming the
|
|
931
|
+
// rows it left, which --rollback reverts once the conflict is gone (T13384).
|
|
932
|
+
let rolledBack = 0;
|
|
933
|
+
let revertError = null;
|
|
934
|
+
try {
|
|
935
|
+
rolledBack = await revertReconcile(liveStorePath, stagingDir);
|
|
936
|
+
}
|
|
937
|
+
catch (error) {
|
|
938
|
+
revertError = error instanceof Error ? error.message : String(error);
|
|
939
|
+
}
|
|
869
940
|
const cause = !migrated.ok
|
|
870
941
|
? `copy failed: ${migrated.error ?? `the copy engine gave no message — inspect the journal in ${stagingDir}`}`
|
|
871
942
|
: lost.length > 0
|
|
@@ -874,9 +945,13 @@ async function reconcileWithScratch(projectRoot, dryRun, additive, liveStorePath
|
|
|
874
945
|
? `pre-existing live rows changed in: ${altered.join(', ')}`
|
|
875
946
|
: unlanded.length > 0
|
|
876
947
|
? `a concurrent write took the id of a recovered task (${unlanded.join(', ')}); run the reconcile again`
|
|
877
|
-
:
|
|
878
|
-
? `
|
|
879
|
-
:
|
|
948
|
+
: unaccounted.length > 0
|
|
949
|
+
? `stranded rows neither copied nor listed as skipped: ${describeGaps(unaccounted)}`
|
|
950
|
+
: dangling.length > 0
|
|
951
|
+
? `copied rows refer to tasks cleo.db does not hold: ${dangling.join(', ')}`
|
|
952
|
+
: unrecorded !== null
|
|
953
|
+
? `could not record the carried bare tables in the store: ${unrecorded}`
|
|
954
|
+
: `rows still missing after copy: ${describeGaps(after)}`;
|
|
880
955
|
return {
|
|
881
956
|
...base,
|
|
882
957
|
outcome: 'refused',
|
|
@@ -885,13 +960,11 @@ async function reconcileWithScratch(projectRoot, dryRun, additive, liveStorePath
|
|
|
885
960
|
rowsCopied,
|
|
886
961
|
rolledBack,
|
|
887
962
|
stagingDir,
|
|
888
|
-
reason:
|
|
963
|
+
reason: revertError === null
|
|
964
|
+
? `${cause} — reverted the ${rolledBack} row(s) this run inserted; legacy files untouched`
|
|
965
|
+
: `${cause} — the revert FAILED (${revertError}): the ${rowsCopied} row(s) this run inserted are still in cleo.db; once that is resolved, \`cleo doctor superseded-store --rollback ${basename(stagingDir)}\` reverts them; legacy files untouched`,
|
|
889
966
|
};
|
|
890
967
|
}), { stale: EXODUS_LOCK_STALE_MS, retries: 30, onCompromised: lock.onCompromised });
|
|
891
|
-
|
|
892
|
-
const receipt = { ...result, receiptPath };
|
|
893
|
-
writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, 'utf8');
|
|
894
|
-
log.info({ outcome: receipt.outcome, rowsCopied: receipt.rowsCopied, receiptPath }, `exodus-reconcile: ${receipt.reason}`);
|
|
895
|
-
return receipt;
|
|
968
|
+
return writeReceipt(result, stagingDir);
|
|
896
969
|
}
|
|
897
970
|
//# sourceMappingURL=reconcile.js.map
|