@cleocode/core 2026.10.17 → 2026.10.18-rc.ci.688

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.
Files changed (188) hide show
  1. package/dist/audit-prune.d.ts +37 -8
  2. package/dist/audit-prune.d.ts.map +1 -1
  3. package/dist/audit-prune.js +46 -12
  4. package/dist/audit-prune.js.map +1 -1
  5. package/dist/build-provenance.json +2 -2
  6. package/dist/cloud/manifest-check.d.ts +21 -0
  7. package/dist/cloud/manifest-check.d.ts.map +1 -1
  8. package/dist/cloud/manifest-check.js +46 -1
  9. package/dist/cloud/manifest-check.js.map +1 -1
  10. package/dist/cloud/nexus-vault.d.ts +19 -0
  11. package/dist/cloud/nexus-vault.d.ts.map +1 -1
  12. package/dist/cloud/nexus-vault.js +134 -23
  13. package/dist/cloud/nexus-vault.js.map +1 -1
  14. package/dist/cloud/signing.d.ts.map +1 -1
  15. package/dist/cloud/signing.js +4 -0
  16. package/dist/cloud/signing.js.map +1 -1
  17. package/dist/doctor/row-identity.d.ts.map +1 -1
  18. package/dist/doctor/row-identity.js +2 -1
  19. package/dist/doctor/row-identity.js.map +1 -1
  20. package/dist/git/work-tree.d.ts +17 -0
  21. package/dist/git/work-tree.d.ts.map +1 -1
  22. package/dist/git/work-tree.js +30 -0
  23. package/dist/git/work-tree.js.map +1 -1
  24. package/dist/goal/close.d.ts +95 -0
  25. package/dist/goal/close.d.ts.map +1 -0
  26. package/dist/goal/close.js +497 -0
  27. package/dist/goal/close.js.map +1 -0
  28. package/dist/goal/draft.d.ts +81 -0
  29. package/dist/goal/draft.d.ts.map +1 -0
  30. package/dist/goal/draft.js +331 -0
  31. package/dist/goal/draft.js.map +1 -0
  32. package/dist/hooks/release-gate.d.ts +3 -0
  33. package/dist/hooks/release-gate.d.ts.map +1 -1
  34. package/dist/hooks/release-gate.js +4 -1
  35. package/dist/hooks/release-gate.js.map +1 -1
  36. package/dist/orchestration/lean-gain.d.ts +26 -0
  37. package/dist/orchestration/lean-gain.d.ts.map +1 -0
  38. package/dist/orchestration/lean-gain.js +61 -0
  39. package/dist/orchestration/lean-gain.js.map +1 -0
  40. package/dist/release/backfill.d.ts.map +1 -1
  41. package/dist/release/backfill.js +15 -5
  42. package/dist/release/backfill.js.map +1 -1
  43. package/dist/release/ci-evidence.d.ts +90 -5
  44. package/dist/release/ci-evidence.d.ts.map +1 -1
  45. package/dist/release/ci-evidence.js +277 -4
  46. package/dist/release/ci-evidence.js.map +1 -1
  47. package/dist/release/plan.d.ts +4 -1
  48. package/dist/release/plan.d.ts.map +1 -1
  49. package/dist/release/plan.js +10 -2
  50. package/dist/release/plan.js.map +1 -1
  51. package/dist/release/reconcile.d.ts +10 -0
  52. package/dist/release/reconcile.d.ts.map +1 -1
  53. package/dist/release/reconcile.js +42 -19
  54. package/dist/release/reconcile.js.map +1 -1
  55. package/dist/release/release-manifest.d.ts +2 -1
  56. package/dist/release/release-manifest.d.ts.map +1 -1
  57. package/dist/release/release-manifest.js +15 -1
  58. package/dist/release/release-manifest.js.map +1 -1
  59. package/dist/resources/admission-ledger.d.ts +21 -4
  60. package/dist/resources/admission-ledger.d.ts.map +1 -1
  61. package/dist/resources/admission-ledger.js +102 -19
  62. package/dist/resources/admission-ledger.js.map +1 -1
  63. package/dist/resources/run-class.d.ts +22 -0
  64. package/dist/resources/run-class.d.ts.map +1 -1
  65. package/dist/resources/run-class.js +81 -0
  66. package/dist/resources/run-class.js.map +1 -1
  67. package/dist/sessions/handoff.d.ts +2 -2
  68. package/dist/sessions/handoff.js +2 -2
  69. package/dist/store/audit-log-schema.d.ts +106 -39
  70. package/dist/store/audit-log-schema.d.ts.map +1 -1
  71. package/dist/store/audit-log-schema.js +6 -3
  72. package/dist/store/audit-log-schema.js.map +1 -1
  73. package/dist/store/row-identity-registry.d.ts +42 -6
  74. package/dist/store/row-identity-registry.d.ts.map +1 -1
  75. package/dist/store/row-identity-registry.js +256 -34
  76. package/dist/store/row-identity-registry.js.map +1 -1
  77. package/dist/store/row-identity.d.ts.map +1 -1
  78. package/dist/store/row-identity.js +16 -4
  79. package/dist/store/row-identity.js.map +1 -1
  80. package/dist/store/schema/cleo-global/nexus.d.ts +16 -0
  81. package/dist/store/schema/cleo-global/nexus.d.ts.map +1 -1
  82. package/dist/store/schema/cleo-global/nexus.js +4 -1
  83. package/dist/store/schema/cleo-global/nexus.js.map +1 -1
  84. package/dist/store/schema/cleo-project/audit.d.ts +48 -0
  85. package/dist/store/schema/cleo-project/audit.d.ts.map +1 -1
  86. package/dist/store/schema/cleo-project/audit.js +15 -1
  87. package/dist/store/schema/cleo-project/audit.js.map +1 -1
  88. package/dist/store/schema/cleo-project/lifecycle.d.ts +112 -0
  89. package/dist/store/schema/cleo-project/lifecycle.d.ts.map +1 -1
  90. package/dist/store/schema/cleo-project/lifecycle.js +36 -4
  91. package/dist/store/schema/cleo-project/lifecycle.js.map +1 -1
  92. package/dist/store/schema/cleo-project/provenance-commits.d.ts +48 -0
  93. package/dist/store/schema/cleo-project/provenance-commits.d.ts.map +1 -1
  94. package/dist/store/schema/cleo-project/provenance-commits.js +10 -1
  95. package/dist/store/schema/cleo-project/provenance-commits.js.map +1 -1
  96. package/dist/store/schema/cleo-project/provenance-rest.d.ts +128 -0
  97. package/dist/store/schema/cleo-project/provenance-rest.d.ts.map +1 -1
  98. package/dist/store/schema/cleo-project/provenance-rest.js +25 -1
  99. package/dist/store/schema/cleo-project/provenance-rest.js.map +1 -1
  100. package/dist/store/schema/cleo-project/tasks-core.d.ts +16 -0
  101. package/dist/store/schema/cleo-project/tasks-core.d.ts.map +1 -1
  102. package/dist/store/schema/cleo-project/tasks-core.js +6 -1
  103. package/dist/store/schema/cleo-project/tasks-core.js.map +1 -1
  104. package/dist/store/session-store.d.ts +5 -4
  105. package/dist/store/session-store.d.ts.map +1 -1
  106. package/dist/store/session-store.js +19 -5
  107. package/dist/store/session-store.js.map +1 -1
  108. package/dist/store/sync/apply/parent-delete.d.ts.map +1 -1
  109. package/dist/store/sync/apply/parent-delete.js +8 -0
  110. package/dist/store/sync/apply/parent-delete.js.map +1 -1
  111. package/dist/store/sync/apply/write-api.d.ts +3 -2
  112. package/dist/store/sync/apply/write-api.d.ts.map +1 -1
  113. package/dist/store/sync/apply/write-api.js +19 -0
  114. package/dist/store/sync/apply/write-api.js.map +1 -1
  115. package/dist/store/sync/capture.d.ts +11 -0
  116. package/dist/store/sync/capture.d.ts.map +1 -1
  117. package/dist/store/sync/capture.js +13 -3
  118. package/dist/store/sync/capture.js.map +1 -1
  119. package/dist/store/sync/flags.d.ts +10 -0
  120. package/dist/store/sync/flags.d.ts.map +1 -1
  121. package/dist/store/sync/flags.js +43 -5
  122. package/dist/store/sync/flags.js.map +1 -1
  123. package/dist/store/sync/timestamp-canon.d.ts.map +1 -1
  124. package/dist/store/sync/timestamp-canon.js +37 -0
  125. package/dist/store/sync/timestamp-canon.js.map +1 -1
  126. package/dist/store/tasks-schema.d.ts +1 -1
  127. package/dist/store/tasks-schema.d.ts.map +1 -1
  128. package/dist/store/tasks-schema.js +5 -1
  129. package/dist/store/tasks-schema.js.map +1 -1
  130. package/dist/store/twin-collapse.d.ts +2 -1
  131. package/dist/store/twin-collapse.d.ts.map +1 -1
  132. package/dist/store/twin-collapse.js +128 -104
  133. package/dist/store/twin-collapse.js.map +1 -1
  134. package/dist/store/validation-schemas.d.ts +390 -38
  135. package/dist/store/validation-schemas.d.ts.map +1 -1
  136. package/dist/store/vault-manifest.d.ts +9 -2
  137. package/dist/store/vault-manifest.d.ts.map +1 -1
  138. package/dist/store/vault-manifest.js +25 -4
  139. package/dist/store/vault-manifest.js.map +1 -1
  140. package/dist/system/health.d.ts.map +1 -1
  141. package/dist/system/health.js +3 -2
  142. package/dist/system/health.js.map +1 -1
  143. package/dist/tasks/affected-packages.d.ts +21 -2
  144. package/dist/tasks/affected-packages.d.ts.map +1 -1
  145. package/dist/tasks/affected-packages.js +96 -8
  146. package/dist/tasks/affected-packages.js.map +1 -1
  147. package/dist/tasks/affected-scope.d.ts +7 -2
  148. package/dist/tasks/affected-scope.d.ts.map +1 -1
  149. package/dist/tasks/affected-scope.js +158 -14
  150. package/dist/tasks/affected-scope.js.map +1 -1
  151. package/dist/tasks/change-set.d.ts +10 -0
  152. package/dist/tasks/change-set.d.ts.map +1 -1
  153. package/dist/tasks/change-set.js +70 -3
  154. package/dist/tasks/change-set.js.map +1 -1
  155. package/dist/tasks/done-plan.d.ts +11 -5
  156. package/dist/tasks/done-plan.d.ts.map +1 -1
  157. package/dist/tasks/done-plan.js +54 -10
  158. package/dist/tasks/done-plan.js.map +1 -1
  159. package/dist/tasks/done-record.d.ts.map +1 -1
  160. package/dist/tasks/done-record.js +25 -0
  161. package/dist/tasks/done-record.js.map +1 -1
  162. package/dist/tasks/evidence.d.ts.map +1 -1
  163. package/dist/tasks/evidence.js +22 -2
  164. package/dist/tasks/evidence.js.map +1 -1
  165. package/dist/tasks/gate-result-cache.d.ts.map +1 -1
  166. package/dist/tasks/gate-result-cache.js +24 -3
  167. package/dist/tasks/gate-result-cache.js.map +1 -1
  168. package/dist/tasks/gate-runner.d.ts +27 -3
  169. package/dist/tasks/gate-runner.d.ts.map +1 -1
  170. package/dist/tasks/gate-runner.js +77 -29
  171. package/dist/tasks/gate-runner.js.map +1 -1
  172. package/dist/tasks/qa-run-binding.d.ts +22 -3
  173. package/dist/tasks/qa-run-binding.d.ts.map +1 -1
  174. package/dist/tasks/qa-run-binding.js +46 -4
  175. package/dist/tasks/qa-run-binding.js.map +1 -1
  176. package/dist/tasks/test-run-binding.d.ts +21 -3
  177. package/dist/tasks/test-run-binding.d.ts.map +1 -1
  178. package/dist/tasks/test-run-binding.js +60 -14
  179. package/dist/tasks/test-run-binding.js.map +1 -1
  180. package/dist/tasks/tool-cache.d.ts.map +1 -1
  181. package/dist/tasks/tool-cache.js +24 -12
  182. package/dist/tasks/tool-cache.js.map +1 -1
  183. package/migrations/drizzle-cleo-global/20261016130000_t13777-nexus-audit-row-uids/migration.sql +18 -0
  184. package/migrations/drizzle-cleo-project/20261015100000_t12914-lifecycle-release-row-uids/migration.sql +88 -0
  185. package/migrations/drizzle-cleo-project/20261016130000_t13777-log-row-uids/migration.sql +42 -0
  186. package/migrations/drizzle-cleo-project/20261016140000_t13777-audit-collapse/migration.sql +31 -0
  187. package/package.json +18 -18
  188. 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
- if (format === VAULT_MANIFEST_FORMAT_VERSION)
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 vault = buildVaultManifest(dbPath, { scope: tableScopeOf(t), hashKey, root }).manifest;
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 { bundlePath, vault, gitTracked: snapshotGitTracked(bundleManifest, t), files, dbPath };
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, { scope: tableScopeOf(t), hashKey, root }).manifest;
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
- const { tallies, unexplained } = settleShortfalls(taken.read.tallies, local, keys.filter((k) => !isSnapshotAuthoritativeKey(k)));
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
- throw vaultError('E_NEXUS_VAULT_REFUSED', `this store's rows do not reconcile with ${t.streamId}'s change journal (${unexplained.slice(0, 8).join(', ')}): a write landed outside the journal`, 'nothing was written; run `cleo cloud sync`, then push again');
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
- if (format !== null && format !== VAULT_MANIFEST_FORMAT_VERSION) {
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 ? await localManifest(t, git, untrackedComparison(t, git, synced)) : null;
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 };