@cleocode/core 2026.10.17 → 2026.10.18
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/audit-prune.d.ts +37 -8
- package/dist/audit-prune.d.ts.map +1 -1
- package/dist/audit-prune.js +46 -12
- package/dist/audit-prune.js.map +1 -1
- package/dist/build-provenance.json +2 -2
- package/dist/cloud/manifest-check.d.ts +21 -0
- package/dist/cloud/manifest-check.d.ts.map +1 -1
- package/dist/cloud/manifest-check.js +46 -1
- package/dist/cloud/manifest-check.js.map +1 -1
- package/dist/cloud/nexus-vault.d.ts +19 -0
- package/dist/cloud/nexus-vault.d.ts.map +1 -1
- package/dist/cloud/nexus-vault.js +134 -23
- package/dist/cloud/nexus-vault.js.map +1 -1
- package/dist/cloud/signing.d.ts.map +1 -1
- package/dist/cloud/signing.js +4 -0
- package/dist/cloud/signing.js.map +1 -1
- package/dist/doctor/row-identity.d.ts.map +1 -1
- package/dist/doctor/row-identity.js +2 -1
- package/dist/doctor/row-identity.js.map +1 -1
- package/dist/git/work-tree.d.ts +17 -0
- package/dist/git/work-tree.d.ts.map +1 -1
- package/dist/git/work-tree.js +30 -0
- package/dist/git/work-tree.js.map +1 -1
- package/dist/goal/close.d.ts +95 -0
- package/dist/goal/close.d.ts.map +1 -0
- package/dist/goal/close.js +497 -0
- package/dist/goal/close.js.map +1 -0
- package/dist/goal/draft.d.ts +81 -0
- package/dist/goal/draft.d.ts.map +1 -0
- package/dist/goal/draft.js +331 -0
- package/dist/goal/draft.js.map +1 -0
- package/dist/hooks/release-gate.d.ts +3 -0
- package/dist/hooks/release-gate.d.ts.map +1 -1
- package/dist/hooks/release-gate.js +4 -1
- package/dist/hooks/release-gate.js.map +1 -1
- package/dist/orchestration/lean-gain.d.ts +26 -0
- package/dist/orchestration/lean-gain.d.ts.map +1 -0
- package/dist/orchestration/lean-gain.js +61 -0
- package/dist/orchestration/lean-gain.js.map +1 -0
- package/dist/release/backfill.d.ts.map +1 -1
- package/dist/release/backfill.js +15 -5
- package/dist/release/backfill.js.map +1 -1
- package/dist/release/ci-evidence.d.ts +90 -5
- package/dist/release/ci-evidence.d.ts.map +1 -1
- package/dist/release/ci-evidence.js +277 -4
- package/dist/release/ci-evidence.js.map +1 -1
- package/dist/release/plan.d.ts +4 -1
- package/dist/release/plan.d.ts.map +1 -1
- package/dist/release/plan.js +10 -2
- package/dist/release/plan.js.map +1 -1
- package/dist/release/reconcile.d.ts +10 -0
- package/dist/release/reconcile.d.ts.map +1 -1
- package/dist/release/reconcile.js +42 -19
- package/dist/release/reconcile.js.map +1 -1
- package/dist/release/release-manifest.d.ts +2 -1
- package/dist/release/release-manifest.d.ts.map +1 -1
- package/dist/release/release-manifest.js +15 -1
- package/dist/release/release-manifest.js.map +1 -1
- package/dist/resources/admission-ledger.d.ts +21 -4
- package/dist/resources/admission-ledger.d.ts.map +1 -1
- package/dist/resources/admission-ledger.js +102 -19
- package/dist/resources/admission-ledger.js.map +1 -1
- package/dist/resources/run-class.d.ts +22 -0
- package/dist/resources/run-class.d.ts.map +1 -1
- package/dist/resources/run-class.js +81 -0
- package/dist/resources/run-class.js.map +1 -1
- package/dist/sessions/handoff.d.ts +2 -2
- package/dist/sessions/handoff.js +2 -2
- package/dist/store/audit-log-schema.d.ts +106 -39
- package/dist/store/audit-log-schema.d.ts.map +1 -1
- package/dist/store/audit-log-schema.js +6 -3
- package/dist/store/audit-log-schema.js.map +1 -1
- package/dist/store/row-identity-registry.d.ts +42 -6
- package/dist/store/row-identity-registry.d.ts.map +1 -1
- package/dist/store/row-identity-registry.js +256 -34
- package/dist/store/row-identity-registry.js.map +1 -1
- package/dist/store/row-identity.d.ts.map +1 -1
- package/dist/store/row-identity.js +16 -4
- package/dist/store/row-identity.js.map +1 -1
- package/dist/store/schema/cleo-global/nexus.d.ts +16 -0
- package/dist/store/schema/cleo-global/nexus.d.ts.map +1 -1
- package/dist/store/schema/cleo-global/nexus.js +4 -1
- package/dist/store/schema/cleo-global/nexus.js.map +1 -1
- package/dist/store/schema/cleo-project/audit.d.ts +48 -0
- package/dist/store/schema/cleo-project/audit.d.ts.map +1 -1
- package/dist/store/schema/cleo-project/audit.js +15 -1
- package/dist/store/schema/cleo-project/audit.js.map +1 -1
- package/dist/store/schema/cleo-project/lifecycle.d.ts +112 -0
- package/dist/store/schema/cleo-project/lifecycle.d.ts.map +1 -1
- package/dist/store/schema/cleo-project/lifecycle.js +36 -4
- package/dist/store/schema/cleo-project/lifecycle.js.map +1 -1
- package/dist/store/schema/cleo-project/provenance-commits.d.ts +48 -0
- package/dist/store/schema/cleo-project/provenance-commits.d.ts.map +1 -1
- package/dist/store/schema/cleo-project/provenance-commits.js +10 -1
- package/dist/store/schema/cleo-project/provenance-commits.js.map +1 -1
- package/dist/store/schema/cleo-project/provenance-rest.d.ts +128 -0
- package/dist/store/schema/cleo-project/provenance-rest.d.ts.map +1 -1
- package/dist/store/schema/cleo-project/provenance-rest.js +25 -1
- package/dist/store/schema/cleo-project/provenance-rest.js.map +1 -1
- package/dist/store/schema/cleo-project/tasks-core.d.ts +16 -0
- package/dist/store/schema/cleo-project/tasks-core.d.ts.map +1 -1
- package/dist/store/schema/cleo-project/tasks-core.js +6 -1
- package/dist/store/schema/cleo-project/tasks-core.js.map +1 -1
- package/dist/store/session-store.d.ts +5 -4
- package/dist/store/session-store.d.ts.map +1 -1
- package/dist/store/session-store.js +19 -5
- package/dist/store/session-store.js.map +1 -1
- package/dist/store/sync/apply/parent-delete.d.ts.map +1 -1
- package/dist/store/sync/apply/parent-delete.js +8 -0
- package/dist/store/sync/apply/parent-delete.js.map +1 -1
- package/dist/store/sync/apply/write-api.d.ts +3 -2
- package/dist/store/sync/apply/write-api.d.ts.map +1 -1
- package/dist/store/sync/apply/write-api.js +19 -0
- package/dist/store/sync/apply/write-api.js.map +1 -1
- package/dist/store/sync/capture.d.ts +11 -0
- package/dist/store/sync/capture.d.ts.map +1 -1
- package/dist/store/sync/capture.js +13 -3
- package/dist/store/sync/capture.js.map +1 -1
- package/dist/store/sync/flags.d.ts +10 -0
- package/dist/store/sync/flags.d.ts.map +1 -1
- package/dist/store/sync/flags.js +43 -5
- package/dist/store/sync/flags.js.map +1 -1
- package/dist/store/sync/timestamp-canon.d.ts.map +1 -1
- package/dist/store/sync/timestamp-canon.js +37 -0
- package/dist/store/sync/timestamp-canon.js.map +1 -1
- package/dist/store/tasks-schema.d.ts +1 -1
- package/dist/store/tasks-schema.d.ts.map +1 -1
- package/dist/store/tasks-schema.js +5 -1
- package/dist/store/tasks-schema.js.map +1 -1
- package/dist/store/twin-collapse.d.ts +2 -1
- package/dist/store/twin-collapse.d.ts.map +1 -1
- package/dist/store/twin-collapse.js +128 -104
- package/dist/store/twin-collapse.js.map +1 -1
- package/dist/store/validation-schemas.d.ts +390 -38
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/dist/store/vault-manifest.d.ts +9 -2
- package/dist/store/vault-manifest.d.ts.map +1 -1
- package/dist/store/vault-manifest.js +25 -4
- package/dist/store/vault-manifest.js.map +1 -1
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +3 -2
- package/dist/system/health.js.map +1 -1
- package/dist/tasks/affected-packages.d.ts +21 -2
- package/dist/tasks/affected-packages.d.ts.map +1 -1
- package/dist/tasks/affected-packages.js +96 -8
- package/dist/tasks/affected-packages.js.map +1 -1
- package/dist/tasks/affected-scope.d.ts +7 -2
- package/dist/tasks/affected-scope.d.ts.map +1 -1
- package/dist/tasks/affected-scope.js +158 -14
- package/dist/tasks/affected-scope.js.map +1 -1
- package/dist/tasks/change-set.d.ts +10 -0
- package/dist/tasks/change-set.d.ts.map +1 -1
- package/dist/tasks/change-set.js +70 -3
- package/dist/tasks/change-set.js.map +1 -1
- package/dist/tasks/done-plan.d.ts +11 -5
- package/dist/tasks/done-plan.d.ts.map +1 -1
- package/dist/tasks/done-plan.js +54 -10
- package/dist/tasks/done-plan.js.map +1 -1
- package/dist/tasks/done-record.d.ts.map +1 -1
- package/dist/tasks/done-record.js +25 -0
- package/dist/tasks/done-record.js.map +1 -1
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +22 -2
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/gate-result-cache.d.ts.map +1 -1
- package/dist/tasks/gate-result-cache.js +24 -3
- package/dist/tasks/gate-result-cache.js.map +1 -1
- package/dist/tasks/gate-runner.d.ts +27 -3
- package/dist/tasks/gate-runner.d.ts.map +1 -1
- package/dist/tasks/gate-runner.js +77 -29
- package/dist/tasks/gate-runner.js.map +1 -1
- package/dist/tasks/qa-run-binding.d.ts +22 -3
- package/dist/tasks/qa-run-binding.d.ts.map +1 -1
- package/dist/tasks/qa-run-binding.js +46 -4
- package/dist/tasks/qa-run-binding.js.map +1 -1
- package/dist/tasks/test-run-binding.d.ts +21 -3
- package/dist/tasks/test-run-binding.d.ts.map +1 -1
- package/dist/tasks/test-run-binding.js +60 -14
- package/dist/tasks/test-run-binding.js.map +1 -1
- package/dist/tasks/tool-cache.d.ts.map +1 -1
- package/dist/tasks/tool-cache.js +24 -12
- package/dist/tasks/tool-cache.js.map +1 -1
- package/migrations/drizzle-cleo-global/20261016130000_t13777-nexus-audit-row-uids/migration.sql +18 -0
- package/migrations/drizzle-cleo-project/20261015100000_t12914-lifecycle-release-row-uids/migration.sql +88 -0
- package/migrations/drizzle-cleo-project/20261016130000_t13777-log-row-uids/migration.sql +42 -0
- package/migrations/drizzle-cleo-project/20261016140000_t13777-audit-collapse/migration.sql +31 -0
- package/package.json +18 -18
- package/templates/CLEO-REFERENCE.md +1 -1
|
@@ -57,7 +57,7 @@ import fs from 'node:fs';
|
|
|
57
57
|
import os from 'node:os';
|
|
58
58
|
import path from 'node:path';
|
|
59
59
|
import { ListCheckpointsResult, ListHomeReplicasResult, ListLeasesResult, ReplayPin, } from '@cleocode/contracts/cloud';
|
|
60
|
-
import { NEXUS_CLOUD_MAX_PAGES, NEXUS_PAGE_LIMIT_MAX, nexusCloudDevicePageSchema, nexusCloudReplicaPageSchema, nexusCloudStatusSchema, } from '@cleocode/contracts/nexus-cloud.js';
|
|
60
|
+
import { NEXUS_CLOUD_MAX_PAGES, NEXUS_FEATURE_MANIFEST_SNAPSHOT_AUTHORITATIVE, NEXUS_PAGE_LIMIT_MAX, nexusCloudDevicePageSchema, nexusCloudReplicaPageSchema, nexusCloudStatusSchema, } from '@cleocode/contracts/nexus-cloud.js';
|
|
61
61
|
import { NEXUS_VAULT_LEASE_ROLE, nexusLeaseAcquireSchema, nexusStreamHeadSchema, } from '@cleocode/contracts/nexus-vault.js';
|
|
62
62
|
import { SYNC_SCHEMA_VERSION } from '@cleocode/contracts/sync-schema.js';
|
|
63
63
|
import { readDeclaredProjectIdentity } from '@cleocode/paths';
|
|
@@ -69,6 +69,7 @@ import { exportPortableBundle, globalHomeRules } from '../store/portable-bundle.
|
|
|
69
69
|
import { extractPortableBundle, importPortableBundle } from '../store/portable-bundle-import.js';
|
|
70
70
|
import { integrityCheck, PROJECT_SECTION_RULES, scanSection, sha256File, } from '../store/portable-bundle-scan.js';
|
|
71
71
|
import { writeRestoreMarker } from '../store/restore-marker.js';
|
|
72
|
+
import { ROW_IDENTITY_EXEMPT } from '../store/row-identity-registry.js';
|
|
72
73
|
import { FIRST_OPEN_LOCK_SUFFIX } from '../store/sqlite.js';
|
|
73
74
|
import { isSyncFlagOn, setSyncFlag, UNRELEASED_FLAGS } from '../store/sync/flags.js';
|
|
74
75
|
import { completeGenesis, cutGenesisWithSnapshot, GenesisRacedError, GenesisTakenOverError, genesisCutOf, genesisPending, joinStream, readGenesisCut, } from '../store/sync/genesis.js';
|
|
@@ -422,7 +423,8 @@ function formatWarning(t, cp) {
|
|
|
422
423
|
if (!cp)
|
|
423
424
|
return null;
|
|
424
425
|
const format = vaultManifestFormat(cp.manifest, hashKeyOf(t.dataKey));
|
|
425
|
-
|
|
426
|
+
// Format 2 is compared in its own terms (local rows counted, T13808).
|
|
427
|
+
if (format === VAULT_MANIFEST_FORMAT_VERSION || format === 2)
|
|
426
428
|
return null;
|
|
427
429
|
return {
|
|
428
430
|
code: 'W_NEXUS_VAULT_FORMAT',
|
|
@@ -702,8 +704,10 @@ function comparable(m, git) {
|
|
|
702
704
|
* The vault manifest of an extracted bundle: the syncing tables of its
|
|
703
705
|
* `cleo.db`, one entry per other database of the section, and one entry for
|
|
704
706
|
* its plain-file inventory (T12969), so a change anywhere in the bundle is seen.
|
|
707
|
+
* Checked against a checkpoint hashed under format 2 (`like`), rows local by
|
|
708
|
+
* row-identity spec are counted as that format did (T13808).
|
|
705
709
|
*/
|
|
706
|
-
async function snapshotManifest(extractDir, manifest, t) {
|
|
710
|
+
async function snapshotManifest(extractDir, manifest, t, like) {
|
|
707
711
|
const section = t.scope === 'global' ? manifest.global?.home : manifest.projects[0];
|
|
708
712
|
const entry = section?.databases.find((d) => d.role === 'primary' && d.relPath === 'cleo.db');
|
|
709
713
|
if (!section || !entry) {
|
|
@@ -714,7 +718,14 @@ async function snapshotManifest(extractDir, manifest, t) {
|
|
|
714
718
|
const root = t.scope === 'global' ? null : (manifest.projects[0]?.originalPath ?? section.originalRoot);
|
|
715
719
|
const hashKey = hashKeyOf(t.dataKey);
|
|
716
720
|
const dbPath = path.join(extractDir, entry.bundlePath);
|
|
717
|
-
const
|
|
721
|
+
const likeFormat = like ? vaultManifestFormat(like, hashKey) : VAULT_MANIFEST_FORMAT_VERSION;
|
|
722
|
+
const built = buildVaultManifest(dbPath, {
|
|
723
|
+
scope: tableScopeOf(t),
|
|
724
|
+
hashKey,
|
|
725
|
+
root,
|
|
726
|
+
countLocalRows: likeFormat === 2,
|
|
727
|
+
});
|
|
728
|
+
const vault = built.manifest;
|
|
718
729
|
for (const d of section.databases) {
|
|
719
730
|
if (d === entry)
|
|
720
731
|
continue;
|
|
@@ -738,7 +749,7 @@ async function snapshotManifest(extractDir, manifest, t) {
|
|
|
738
749
|
files.push({ relPath: f.relPath, sha256: digest });
|
|
739
750
|
}
|
|
740
751
|
vault.tables[VAULT_FILES_KEY] = vaultFilesEntry(files, hashKey);
|
|
741
|
-
return { vault, dbPath, files: digests };
|
|
752
|
+
return { vault, dbPath, files: digests, localRows: built.localRows };
|
|
742
753
|
}
|
|
743
754
|
/** Export and extract the store into `work`, returning its bundle and vault manifest. */
|
|
744
755
|
async function exportAndRead(t, work, label, tracked, seal, inspect) {
|
|
@@ -748,8 +759,15 @@ async function exportAndRead(t, work, label, tracked, seal, inspect) {
|
|
|
748
759
|
fs.mkdirSync(extractDir);
|
|
749
760
|
await tarExtract({ file: bundlePath, cwd: extractDir });
|
|
750
761
|
const bundleManifest = JSON.parse(fs.readFileSync(path.join(extractDir, 'manifest.json'), 'utf8'));
|
|
751
|
-
const { vault, files, dbPath } = await snapshotManifest(extractDir, bundleManifest, t);
|
|
752
|
-
return {
|
|
762
|
+
const { vault, files, dbPath, localRows } = await snapshotManifest(extractDir, bundleManifest, t);
|
|
763
|
+
return {
|
|
764
|
+
bundlePath,
|
|
765
|
+
vault,
|
|
766
|
+
gitTracked: snapshotGitTracked(bundleManifest, t),
|
|
767
|
+
files,
|
|
768
|
+
dbPath,
|
|
769
|
+
localRows,
|
|
770
|
+
};
|
|
753
771
|
}
|
|
754
772
|
/** The paths a project snapshot's bundle marks as git-tracked; a global one has none (T13019, T13038). */
|
|
755
773
|
function snapshotGitTracked(manifest, t) {
|
|
@@ -769,15 +787,22 @@ function snapshotGitTracked(manifest, t) {
|
|
|
769
787
|
* in `untracked` (git-tracked here, or marked by the snapshot it is compared
|
|
770
788
|
* with) are left out of the inventory hash, and every inventory file's digest
|
|
771
789
|
* is returned for a path-by-path comparison (T13038); with tracking unknown
|
|
772
|
-
* the file inventory is left out entirely (T13019).
|
|
790
|
+
* the file inventory is left out entirely (T13019). Compared with a
|
|
791
|
+
* checkpoint hashed under format 2 (`like`), rows local by row-identity spec
|
|
792
|
+
* are counted as that format did (T13808).
|
|
773
793
|
*/
|
|
774
|
-
async function localManifest(t, git, untracked) {
|
|
794
|
+
async function localManifest(t, git, untracked, like) {
|
|
775
795
|
if (!fs.existsSync(t.dbPath))
|
|
776
796
|
return null;
|
|
777
797
|
const { sectionRoot, scan, primaryRel } = sectionScan(t);
|
|
778
798
|
const hashKey = hashKeyOf(t.dataKey);
|
|
779
799
|
const root = t.scope === 'global' ? null : t.storeRoot;
|
|
780
|
-
const vault = buildVaultManifest(t.dbPath, {
|
|
800
|
+
const vault = buildVaultManifest(t.dbPath, {
|
|
801
|
+
scope: tableScopeOf(t),
|
|
802
|
+
hashKey,
|
|
803
|
+
root,
|
|
804
|
+
countLocalRows: like !== undefined && vaultManifestFormat(like, hashKey) === 2,
|
|
805
|
+
}).manifest;
|
|
781
806
|
for (const rel of scan.sqlite) {
|
|
782
807
|
if (rel === primaryRel)
|
|
783
808
|
continue;
|
|
@@ -941,6 +966,53 @@ async function appendCheckpoint(a) {
|
|
|
941
966
|
}
|
|
942
967
|
return { cp, deltaSegmentSeq, cursor: pushedWith };
|
|
943
968
|
}
|
|
969
|
+
/**
|
|
970
|
+
* Whether a refusal names an omitted snapshot-authoritative list (T13777): a
|
|
971
|
+
* parent checkpoint carried one and this push did not, the mark of a device on
|
|
972
|
+
* a cleo that predates the list.
|
|
973
|
+
*/
|
|
974
|
+
function authoritativeOmitted(err) {
|
|
975
|
+
if (!(err instanceof NexusError) || err.code !== 'E_MANIFEST_ACCOUNTING')
|
|
976
|
+
return false;
|
|
977
|
+
const verdict = err.details?.['verdict'];
|
|
978
|
+
const findings = typeof verdict === 'object' && verdict !== null && 'findings' in verdict
|
|
979
|
+
? verdict.findings
|
|
980
|
+
: null;
|
|
981
|
+
return (Array.isArray(findings) &&
|
|
982
|
+
findings.some((f) => typeof f === 'object' &&
|
|
983
|
+
f !== null &&
|
|
984
|
+
'reason' in f &&
|
|
985
|
+
f.reason === 'authoritative-omitted'));
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* The rows a parent hashed under an older vault format counted that this
|
|
989
|
+
* format leaves out (T13808): rows local by row-identity spec. They never
|
|
990
|
+
* travelled, so the journal declares no delete for them; the checkpoint names
|
|
991
|
+
* them `pruned`, so the server's row rule takes them off the parent's count.
|
|
992
|
+
* Per table, the shortfall against the tallies, capped by the local rows this
|
|
993
|
+
* store holds now; a parent in the current format prunes nothing.
|
|
994
|
+
*
|
|
995
|
+
* @param parentFormat - The parent's {@link vaultManifestFormat}.
|
|
996
|
+
* @param tallies - The window's tallies.
|
|
997
|
+
* @param local - The store's rows per manifest key.
|
|
998
|
+
* @param localRows - Per table, the rows the manifest left out as local.
|
|
999
|
+
* @returns The tallies less the pruned rows, and the `pruned` map.
|
|
1000
|
+
*/
|
|
1001
|
+
export function formatTransitionPruned(parentFormat, tallies, local, localRows) {
|
|
1002
|
+
const pruned = {};
|
|
1003
|
+
if (parentFormat === 'unknown' || parentFormat >= VAULT_MANIFEST_FORMAT_VERSION) {
|
|
1004
|
+
return { tallies, pruned };
|
|
1005
|
+
}
|
|
1006
|
+
const rows = { ...tallies.rows };
|
|
1007
|
+
for (const [table, n] of Object.entries(localRows)) {
|
|
1008
|
+
const take = Math.min(n, (rows[table] ?? 0) - (local[table] ?? 0));
|
|
1009
|
+
if (take <= 0)
|
|
1010
|
+
continue;
|
|
1011
|
+
pruned[table] = take;
|
|
1012
|
+
rows[table] = (rows[table] ?? 0) - take;
|
|
1013
|
+
}
|
|
1014
|
+
return { tallies: { ...tallies, rows }, pruned };
|
|
1015
|
+
}
|
|
944
1016
|
/** Whether this store's `sync.push` is on: its vault push checkpoints the journal (T12999). */
|
|
945
1017
|
async function journalPushOn(t) {
|
|
946
1018
|
const { openNativeDatabase } = await import('../store/sqlite-native.js');
|
|
@@ -987,6 +1059,30 @@ async function snapshotJournalState(snapshotPath, stream, replicaId) {
|
|
|
987
1059
|
db.close();
|
|
988
1060
|
}
|
|
989
1061
|
}
|
|
1062
|
+
/**
|
|
1063
|
+
* The tables a journal checkpoint names as snapshot-authoritative (T13777): the manifest's portable
|
|
1064
|
+
* tables that are still exempt from row identity, so the change journal never carries them and no
|
|
1065
|
+
* segment declares their rows. Kept to the parent's list when it has one (the server allows only a
|
|
1066
|
+
* shrinking list). `list` is null when the server does not take the field
|
|
1067
|
+
* ({@link NEXUS_FEATURE_MANIFEST_SNAPSHOT_AUTHORITATIVE} absent from E3), so the manifest omits it.
|
|
1068
|
+
*/
|
|
1069
|
+
async function snapshotAuthoritativeTables(conn, t, keys, parent) {
|
|
1070
|
+
const exempt = new Set(Object.keys(ROW_IDENTITY_EXEMPT[tableScopeOf(t)]));
|
|
1071
|
+
const allowed = parent.manifest.snapshotAuthoritative;
|
|
1072
|
+
// A parent with a list proves the server takes it, and omitting it would be refused as a reset.
|
|
1073
|
+
if (allowed === undefined) {
|
|
1074
|
+
const status = await conn.find(t.projectId !== null
|
|
1075
|
+
? nexusQueryPath('/v1/status', { projectId: t.projectId })
|
|
1076
|
+
: '/v1/status', nexusCloudStatusSchema);
|
|
1077
|
+
if (!(status?.features ?? []).includes(NEXUS_FEATURE_MANIFEST_SNAPSHOT_AUTHORITATIVE)) {
|
|
1078
|
+
return { list: null, exempt };
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return {
|
|
1082
|
+
list: keys.filter((k) => exempt.has(k) && (allowed === undefined || allowed.includes(k))),
|
|
1083
|
+
exempt,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
990
1086
|
/**
|
|
991
1087
|
* `cleo cloud push` on a store that writes its stream's change journal
|
|
992
1088
|
* (`sync.push` on; T12999, journal spec §2.11): seal, push and pull the
|
|
@@ -1049,7 +1145,7 @@ async function pushJournalCheckpoint(conn, key, opts, scope, warnings) {
|
|
|
1049
1145
|
const work = tempDir('cleo-vault-push-');
|
|
1050
1146
|
try {
|
|
1051
1147
|
let snap = null;
|
|
1052
|
-
const { bundlePath, vault, gitTracked, files } = await exportAndRead(t, work, `cloud-vault-${scope}`, trackedForPush(t, git, synced), { masterKey: key.masterKey, warn: (w) => warnings.push(w) }, async (snapshotPath) => {
|
|
1148
|
+
const { bundlePath, vault, gitTracked, files, localRows } = await exportAndRead(t, work, `cloud-vault-${scope}`, trackedForPush(t, git, synced), { masterKey: key.masterKey, warn: (w) => warnings.push(w) }, async (snapshotPath) => {
|
|
1053
1149
|
const state = await snapshotJournalState(snapshotPath, t.streamId, replicaId);
|
|
1054
1150
|
let read = null;
|
|
1055
1151
|
if (state.cursor !== null &&
|
|
@@ -1124,9 +1220,16 @@ async function pushJournalCheckpoint(conn, key, opts, scope, warnings) {
|
|
|
1124
1220
|
const keys = [...new Set([...Object.keys(tables), ...Object.keys(parent.manifest.tables)])]
|
|
1125
1221
|
.filter((k) => !isVaultAnnotationKey(k))
|
|
1126
1222
|
.sort();
|
|
1127
|
-
|
|
1223
|
+
// Portable tables the journal does not carry yet are the snapshot's alone (T13777), named in
|
|
1224
|
+
// the manifest only to a server that takes the list; the list only shrinks along the lineage.
|
|
1225
|
+
const authoritative = await snapshotAuthoritativeTables(conn, t, keys, parent);
|
|
1226
|
+
const formatPruned = formatTransitionPruned(vaultManifestFormat(parent.manifest, hashKeyOf(t.dataKey)), taken.read.tallies, local, localRows);
|
|
1227
|
+
const { tallies, unexplained } = settleShortfalls(formatPruned.tallies, local, keys.filter((k) => !isSnapshotAuthoritativeKey(k) && !(authoritative.list?.includes(k) ?? false)));
|
|
1128
1228
|
if (unexplained.length > 0) {
|
|
1129
|
-
|
|
1229
|
+
const exemptOnly = authoritative.list === null && unexplained.every((k) => authoritative.exempt.has(k));
|
|
1230
|
+
throw vaultError('E_NEXUS_VAULT_REFUSED', `this store's rows do not reconcile with ${t.streamId}'s change journal (${unexplained.slice(0, 8).join(', ')}): ${exemptOnly ? 'these tables are not in the change journal yet, and this Cleo Nexus server does not take a snapshot-authoritative list (manifest.snapshot-authoritative)' : 'a write landed outside the journal'}`, exemptOnly
|
|
1231
|
+
? 'nothing was written; upgrade the Cleo Nexus server, then push again'
|
|
1232
|
+
: 'nothing was written; run `cleo cloud sync`, then push again');
|
|
1130
1233
|
}
|
|
1131
1234
|
const vaultOnlyChanged = keys.some((k) => isSnapshotAuthoritativeKey(k) && local[k] !== (parent.manifest.tables[k]?.rows ?? 0));
|
|
1132
1235
|
const acquired = await acquireLease(conn, t, opts.force === true, opts.force === true ? 'cleo cloud push --force' : 'cleo cloud push');
|
|
@@ -1145,8 +1248,9 @@ async function pushJournalCheckpoint(conn, key, opts, scope, warnings) {
|
|
|
1145
1248
|
pending: tallies.pending,
|
|
1146
1249
|
voided: normalRefDeltas(tallies.voided),
|
|
1147
1250
|
revived: [],
|
|
1148
|
-
pruned:
|
|
1251
|
+
pruned: formatPruned.pruned,
|
|
1149
1252
|
replayPin: pin,
|
|
1253
|
+
...(authoritative.list !== null ? { snapshotAuthoritative: authoritative.list } : {}),
|
|
1150
1254
|
},
|
|
1151
1255
|
cursor: {
|
|
1152
1256
|
after: cursor.after,
|
|
@@ -1160,6 +1264,9 @@ async function pushJournalCheckpoint(conn, key, opts, scope, warnings) {
|
|
|
1160
1264
|
if (err instanceof NexusError && err.code === 'E_LINEAGE') {
|
|
1161
1265
|
throw vaultError('E_NEXUS_VAULT_BEHIND', 'another device pushed a snapshot while this one was uploading', 'run `cleo cloud sync`, then push again');
|
|
1162
1266
|
}
|
|
1267
|
+
if (authoritativeOmitted(err)) {
|
|
1268
|
+
throw vaultError('E_NEXUS_VAULT_REFUSED', `the server refused the snapshot: a device on ${t.streamId} sent a snapshot-authoritative list and this push did not carry it (${err.message})`, 'nothing was written; once one device sends the list, every device on the stream must run a cleo that sends it: upgrade cleo on the other devices, then push again');
|
|
1269
|
+
}
|
|
1163
1270
|
if (err instanceof NexusError &&
|
|
1164
1271
|
(err.code === 'E_REGRESSION' || err.code === 'E_VALIDATION')) {
|
|
1165
1272
|
throw vaultError('E_NEXUS_VAULT_REFUSED', `the server refused the snapshot: ${err.message}`, vaultOnlyChanged
|
|
@@ -1590,7 +1697,7 @@ async function enableSyncPushImpl(opts = {}) {
|
|
|
1590
1697
|
// Unchanged since the restore: the rows match the checkpoint's manifest
|
|
1591
1698
|
// (row data) here, and joinStream checks every row against its meta.
|
|
1592
1699
|
const git = gitTracking(t);
|
|
1593
|
-
const local = await localManifest(t, git, untrackedComparison(t, git, synced));
|
|
1700
|
+
const local = await localManifest(t, git, untrackedComparison(t, git, synced), parent.manifest);
|
|
1594
1701
|
const changed = local === null
|
|
1595
1702
|
? ['(no store)']
|
|
1596
1703
|
: compareVaultManifests(local.manifest, comparable(parent.manifest, git))
|
|
@@ -2648,7 +2755,7 @@ function keepGlobalConfigLocalKeys(configPath, local) {
|
|
|
2648
2755
|
*/
|
|
2649
2756
|
async function pullVaultOnlyEntries(a) {
|
|
2650
2757
|
const { conn, key, t, journal, target, last, git, synced, warnings } = a;
|
|
2651
|
-
const read = await localManifest(t, git, untrackedComparison(t, git, synced));
|
|
2758
|
+
const read = await localManifest(t, git, untrackedComparison(t, git, synced), last?.manifest);
|
|
2652
2759
|
if (read !== null && last !== undefined) {
|
|
2653
2760
|
const fileChanges = fileChangesSinceSync(read.files, t, git, synced);
|
|
2654
2761
|
const changed = compareWithSynced(read.manifest, last.manifest, git, fileChanges)
|
|
@@ -2668,7 +2775,7 @@ async function pullVaultOnlyEntries(a) {
|
|
|
2668
2775
|
const staging = path.join(work, 'staging');
|
|
2669
2776
|
fs.mkdirSync(staging);
|
|
2670
2777
|
const { extractDir, manifest } = await extractPortableBundle(bundlePath, staging);
|
|
2671
|
-
const staged = await snapshotManifest(extractDir, manifest, t);
|
|
2778
|
+
const staged = await snapshotManifest(extractDir, manifest, t, restored.checkpoint.manifest);
|
|
2672
2779
|
const diff = compareVaultManifests(staged.vault, restored.checkpoint.manifest).filter((d) => !d.match);
|
|
2673
2780
|
if (diff.length > 0) {
|
|
2674
2781
|
throw vaultError('E_NEXUS_VAULT_VERIFY_FAILED', `snapshot ${target.checkpointId} does not match its manifest (${diff.map((d) => d.table).join(', ')}); nothing was pulled`);
|
|
@@ -2793,7 +2900,8 @@ async function restoreNexusVaultImpl(opts) {
|
|
|
2793
2900
|
// against its manifest here; say so instead of reporting a mismatch (T13034).
|
|
2794
2901
|
const targetCp = trusted.find((c) => c.checkpointId === target);
|
|
2795
2902
|
const format = targetCp ? vaultManifestFormat(targetCp.manifest, hashKeyOf(t.dataKey)) : null;
|
|
2796
|
-
|
|
2903
|
+
// Format 2 is format 3 with local rows counted: a restore checks it that way (T13808).
|
|
2904
|
+
if (format !== null && format !== VAULT_MANIFEST_FORMAT_VERSION && format !== 2) {
|
|
2797
2905
|
throw vaultError('E_NEXUS_VAULT_VERIFY_FAILED', format === 'unknown'
|
|
2798
2906
|
? `snapshot ${target} records a vault manifest format this CLEO does not recognise (this CLEO hashes format ${VAULT_MANIFEST_FORMAT_VERSION}; a different vault format wrote it, most likely a newer CLEO), so it cannot be verified here; nothing was restored`
|
|
2799
2907
|
: `snapshot ${target} was hashed under vault manifest format ${format}, older than this CLEO's (${VAULT_MANIFEST_FORMAT_VERSION}), so it cannot be verified here; nothing was restored`, format === 'unknown'
|
|
@@ -2829,7 +2937,9 @@ async function restoreNexusVaultImpl(opts) {
|
|
|
2829
2937
|
names,
|
|
2830
2938
|
});
|
|
2831
2939
|
}
|
|
2832
|
-
const local = hasLocal && !force
|
|
2940
|
+
const local = hasLocal && !force
|
|
2941
|
+
? await localManifest(t, git, untrackedComparison(t, git, synced), last?.manifest)
|
|
2942
|
+
: null;
|
|
2833
2943
|
if (local !== null) {
|
|
2834
2944
|
const hasRows = Object.entries(local.manifest.tables).some(([name, x]) => x.rows > 0 && name !== VAULT_FILES_KEY);
|
|
2835
2945
|
const fileChanges = fileChangesSinceSync(local.files, t, git, synced);
|
|
@@ -2910,7 +3020,7 @@ async function restoreNexusVaultImpl(opts) {
|
|
|
2910
3020
|
throw vaultError('E_NEXUS_VAULT_TARGET_OCCUPIED', `${t.storeRoot} already holds project ${present.projectId}, not ${incoming}`, 'pass --into an empty directory, or --force to replace it (a safety backup is taken first)');
|
|
2911
3021
|
}
|
|
2912
3022
|
}
|
|
2913
|
-
const staged = await snapshotManifest(extractDir, manifest, t);
|
|
3023
|
+
const staged = await snapshotManifest(extractDir, manifest, t, restored.checkpoint.manifest);
|
|
2914
3024
|
const diff = compareVaultManifests(staged.vault, restored.checkpoint.manifest).filter((d) => !d.match);
|
|
2915
3025
|
if (diff.length > 0) {
|
|
2916
3026
|
throw vaultError('E_NEXUS_VAULT_VERIFY_FAILED', `snapshot ${target} does not match its manifest (${diff.map((d) => d.table).join(', ')}); nothing was restored`);
|
|
@@ -3108,7 +3218,7 @@ async function nexusVaultStatusImpl(opts = {}) {
|
|
|
3108
3218
|
const git = gitTracking(t);
|
|
3109
3219
|
if (git.mode === 'unknown')
|
|
3110
3220
|
warnings.push(gitUnknownWarning(t));
|
|
3111
|
-
const local = await localManifest(t, git, untrackedComparison(t, git, synced));
|
|
3221
|
+
const local = await localManifest(t, git, untrackedComparison(t, git, synced), last?.manifest);
|
|
3112
3222
|
const pendingChanges = local && last
|
|
3113
3223
|
? compareWithSynced(local.manifest, last.manifest, git, fileChangesSinceSync(local.files, t, git, synced)).filter((d) => !d.match)
|
|
3114
3224
|
: [];
|
|
@@ -3252,6 +3362,7 @@ async function journalVerifySide(journal, signers, t, headCp) {
|
|
|
3252
3362
|
rows,
|
|
3253
3363
|
cloudAdvanced: after < window.head,
|
|
3254
3364
|
localChanged: unpushed > 0 || sealBacklog(db).live > 0,
|
|
3365
|
+
authoritative: new Set(Object.keys(ROW_IDENTITY_EXEMPT[tableScopeOf(t)])),
|
|
3255
3366
|
};
|
|
3256
3367
|
}
|
|
3257
3368
|
finally {
|
|
@@ -3277,7 +3388,7 @@ async function verifyNexusVaultImpl(opts = {}) {
|
|
|
3277
3388
|
const git = gitTracking(t);
|
|
3278
3389
|
if (git.mode === 'unknown')
|
|
3279
3390
|
warnings.push(gitUnknownWarning(t));
|
|
3280
|
-
const read = await localManifest(t, git, untrackedComparison(t, git, synced));
|
|
3391
|
+
const read = await localManifest(t, git, untrackedComparison(t, git, synced), headCp?.manifest);
|
|
3281
3392
|
const local = read?.manifest ?? { schemaVersion: SYNC_SCHEMA_VERSION, tables: {} };
|
|
3282
3393
|
// Files changed since the last sync, path by path (T13038).
|
|
3283
3394
|
const fileChanges = read ? fileChangesSinceSync(read.files, t, git, synced) : null;
|
|
@@ -3294,7 +3405,7 @@ async function verifyNexusVaultImpl(opts = {}) {
|
|
|
3294
3405
|
: null;
|
|
3295
3406
|
if (journalSide !== null) {
|
|
3296
3407
|
tables = tables.map((r) => {
|
|
3297
|
-
if (isSnapshotAuthoritativeKey(r.table))
|
|
3408
|
+
if (isSnapshotAuthoritativeKey(r.table) || journalSide.authoritative.has(r.table))
|
|
3298
3409
|
return r;
|
|
3299
3410
|
const cloudRows = journalSide.rows[r.table] ?? 0;
|
|
3300
3411
|
return { ...r, cloudRows, match: (r.localRows ?? 0) === cloudRows };
|