@cleocode/core 2026.9.24 → 2026.9.25

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 (328) hide show
  1. package/dist/audit-prune.d.ts +32 -0
  2. package/dist/audit-prune.d.ts.map +1 -1
  3. package/dist/audit-prune.js +37 -3
  4. package/dist/audit-prune.js.map +1 -1
  5. package/dist/build-provenance.json +2 -2
  6. package/dist/cloud/crypto.d.ts +1 -2
  7. package/dist/cloud/crypto.d.ts.map +1 -1
  8. package/dist/cloud/crypto.js +1 -11
  9. package/dist/cloud/crypto.js.map +1 -1
  10. package/dist/cloud/uuidv7.d.ts +9 -0
  11. package/dist/cloud/uuidv7.d.ts.map +1 -0
  12. package/dist/cloud/uuidv7.js +19 -0
  13. package/dist/cloud/uuidv7.js.map +1 -0
  14. package/dist/decide/bench/index.d.ts +1 -1
  15. package/dist/decide/bench/index.d.ts.map +1 -1
  16. package/dist/decide/bench/index.js +1 -1
  17. package/dist/decide/bench/index.js.map +1 -1
  18. package/dist/decide/bench/operation.d.ts +1 -1
  19. package/dist/decide/bench/operation.d.ts.map +1 -1
  20. package/dist/decide/bench/operation.js +1 -1
  21. package/dist/decide/bench/operation.js.map +1 -1
  22. package/dist/decide/bench/profiles.d.ts +50 -22
  23. package/dist/decide/bench/profiles.d.ts.map +1 -1
  24. package/dist/decide/bench/profiles.js +103 -59
  25. package/dist/decide/bench/profiles.js.map +1 -1
  26. package/dist/decide/bench/report.d.ts.map +1 -1
  27. package/dist/decide/bench/report.js +6 -1
  28. package/dist/decide/bench/report.js.map +1 -1
  29. package/dist/decide/bench/runner.d.ts +6 -1
  30. package/dist/decide/bench/runner.d.ts.map +1 -1
  31. package/dist/decide/bench/runner.js +1 -0
  32. package/dist/decide/bench/runner.js.map +1 -1
  33. package/dist/decide/client.d.ts.map +1 -1
  34. package/dist/decide/client.js +11 -5
  35. package/dist/decide/client.js.map +1 -1
  36. package/dist/decide/jev-wire.d.ts +31 -1
  37. package/dist/decide/jev-wire.d.ts.map +1 -1
  38. package/dist/decide/jev-wire.js +61 -7
  39. package/dist/decide/jev-wire.js.map +1 -1
  40. package/dist/docs/export-document.js +17181 -15536
  41. package/dist/docs/export-document.js.map +3 -3
  42. package/dist/doctor/orphan-ac-bindings.d.ts +64 -0
  43. package/dist/doctor/orphan-ac-bindings.d.ts.map +1 -0
  44. package/dist/doctor/orphan-ac-bindings.js +59 -0
  45. package/dist/doctor/orphan-ac-bindings.js.map +1 -0
  46. package/dist/doctor/projects.d.ts.map +1 -1
  47. package/dist/doctor/projects.js +43 -29
  48. package/dist/doctor/projects.js.map +1 -1
  49. package/dist/doctor/row-identity.d.ts +31 -0
  50. package/dist/doctor/row-identity.d.ts.map +1 -0
  51. package/dist/doctor/row-identity.js +93 -0
  52. package/dist/doctor/row-identity.js.map +1 -0
  53. package/dist/doctor/twin-collapse.d.ts +58 -2
  54. package/dist/doctor/twin-collapse.d.ts.map +1 -1
  55. package/dist/doctor/twin-collapse.js +212 -14
  56. package/dist/doctor/twin-collapse.js.map +1 -1
  57. package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
  58. package/dist/lifecycle/verification/satisfies-validator.js +10 -0
  59. package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
  60. package/dist/llm/plugin-facade.js +17045 -15400
  61. package/dist/llm/plugin-facade.js.map +3 -3
  62. package/dist/memory/index.d.ts.map +1 -1
  63. package/dist/memory/index.js +19 -10
  64. package/dist/memory/index.js.map +1 -1
  65. package/dist/memory/manifest-identity.d.ts +171 -0
  66. package/dist/memory/manifest-identity.d.ts.map +1 -0
  67. package/dist/memory/manifest-identity.js +345 -0
  68. package/dist/memory/manifest-identity.js.map +1 -0
  69. package/dist/memory/mental-model-queue.d.ts +14 -4
  70. package/dist/memory/mental-model-queue.d.ts.map +1 -1
  71. package/dist/memory/mental-model-queue.js +64 -35
  72. package/dist/memory/mental-model-queue.js.map +1 -1
  73. package/dist/memory/pipeline-manifest-sqlite.d.ts +47 -5
  74. package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
  75. package/dist/memory/pipeline-manifest-sqlite.js +130 -14
  76. package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
  77. package/dist/memory/retrieval/observe.d.ts.map +1 -1
  78. package/dist/memory/retrieval/observe.js +2 -0
  79. package/dist/memory/retrieval/observe.js.map +1 -1
  80. package/dist/memory/transcript-scanner.js +1 -1
  81. package/dist/nexus/deps.d.ts.map +1 -1
  82. package/dist/nexus/deps.js +7 -14
  83. package/dist/nexus/deps.js.map +1 -1
  84. package/dist/nexus/discover.d.ts.map +1 -1
  85. package/dist/nexus/discover.js +6 -9
  86. package/dist/nexus/discover.js.map +1 -1
  87. package/dist/nexus/git-state.d.ts.map +1 -1
  88. package/dist/nexus/git-state.js +10 -1
  89. package/dist/nexus/git-state.js.map +1 -1
  90. package/dist/nexus/projects-clean.d.ts.map +1 -1
  91. package/dist/nexus/projects-clean.js +22 -11
  92. package/dist/nexus/projects-clean.js.map +1 -1
  93. package/dist/nexus/projects-scan.d.ts.map +1 -1
  94. package/dist/nexus/projects-scan.js +8 -13
  95. package/dist/nexus/projects-scan.js.map +1 -1
  96. package/dist/nexus/query.d.ts.map +1 -1
  97. package/dist/nexus/query.js +4 -12
  98. package/dist/nexus/query.js.map +1 -1
  99. package/dist/nexus/registry-roots.d.ts +5 -2
  100. package/dist/nexus/registry-roots.d.ts.map +1 -1
  101. package/dist/nexus/registry-roots.js +8 -10
  102. package/dist/nexus/registry-roots.js.map +1 -1
  103. package/dist/nexus/registry.d.ts +18 -0
  104. package/dist/nexus/registry.d.ts.map +1 -1
  105. package/dist/nexus/registry.js +37 -16
  106. package/dist/nexus/registry.js.map +1 -1
  107. package/dist/orchestrate/worktree-complete.d.ts.map +1 -1
  108. package/dist/orchestrate/worktree-complete.js +40 -2
  109. package/dist/orchestrate/worktree-complete.js.map +1 -1
  110. package/dist/orchestration/spawn-prompt.d.ts +26 -0
  111. package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
  112. package/dist/orchestration/spawn-prompt.js +62 -91
  113. package/dist/orchestration/spawn-prompt.js.map +1 -1
  114. package/dist/release/ci-evidence.d.ts +190 -0
  115. package/dist/release/ci-evidence.d.ts.map +1 -1
  116. package/dist/release/ci-evidence.js +436 -4
  117. package/dist/release/ci-evidence.js.map +1 -1
  118. package/dist/sdk/validator-ac-pull.d.ts.map +1 -1
  119. package/dist/sdk/validator-ac-pull.js +2 -1
  120. package/dist/sdk/validator-ac-pull.js.map +1 -1
  121. package/dist/selfimprove/fix-gen.js +17049 -15404
  122. package/dist/selfimprove/fix-gen.js.map +3 -3
  123. package/dist/sentient/cross-project-hygiene.d.ts +5 -0
  124. package/dist/sentient/cross-project-hygiene.d.ts.map +1 -1
  125. package/dist/sentient/cross-project-hygiene.js +7 -1
  126. package/dist/sentient/cross-project-hygiene.js.map +1 -1
  127. package/dist/sentient/propose-tick.d.ts.map +1 -1
  128. package/dist/sentient/propose-tick.js +2 -3
  129. package/dist/sentient/propose-tick.js.map +1 -1
  130. package/dist/sentient/stage-drift-tick.d.ts.map +1 -1
  131. package/dist/sentient/stage-drift-tick.js +2 -3
  132. package/dist/sentient/stage-drift-tick.js.map +1 -1
  133. package/dist/skills/manifests/research.js +2 -0
  134. package/dist/skills/manifests/research.js.map +1 -1
  135. package/dist/skills/orchestrator/validator.d.ts.map +1 -1
  136. package/dist/skills/orchestrator/validator.js +9 -2
  137. package/dist/skills/orchestrator/validator.js.map +1 -1
  138. package/dist/skills/validation.d.ts +4 -1
  139. package/dist/skills/validation.d.ts.map +1 -1
  140. package/dist/skills/validation.js +11 -3
  141. package/dist/skills/validation.js.map +1 -1
  142. package/dist/spawn/branch-lock.d.ts +111 -7
  143. package/dist/spawn/branch-lock.d.ts.map +1 -1
  144. package/dist/spawn/branch-lock.js +296 -8
  145. package/dist/spawn/branch-lock.js.map +1 -1
  146. package/dist/store/ac-binding-prune.d.ts +144 -0
  147. package/dist/store/ac-binding-prune.d.ts.map +1 -0
  148. package/dist/store/ac-binding-prune.js +293 -0
  149. package/dist/store/ac-binding-prune.js.map +1 -0
  150. package/dist/store/agent-registry-store.d.ts.map +1 -1
  151. package/dist/store/agent-registry-store.js +3 -2
  152. package/dist/store/agent-registry-store.js.map +1 -1
  153. package/dist/store/backup-sidecar.d.ts +85 -2
  154. package/dist/store/backup-sidecar.d.ts.map +1 -1
  155. package/dist/store/backup-sidecar.js +173 -1
  156. package/dist/store/backup-sidecar.js.map +1 -1
  157. package/dist/store/conduit-sqlite.d.ts.map +1 -1
  158. package/dist/store/conduit-sqlite.js +3 -2
  159. package/dist/store/conduit-sqlite.js.map +1 -1
  160. package/dist/store/credential-transfer.d.ts.map +1 -1
  161. package/dist/store/credential-transfer.js +3 -0
  162. package/dist/store/credential-transfer.js.map +1 -1
  163. package/dist/store/db-helpers.d.ts +17 -11
  164. package/dist/store/db-helpers.d.ts.map +1 -1
  165. package/dist/store/db-helpers.js +61 -38
  166. package/dist/store/db-helpers.js.map +1 -1
  167. package/dist/store/display-id-alias.d.ts +529 -0
  168. package/dist/store/display-id-alias.d.ts.map +1 -0
  169. package/dist/store/display-id-alias.js +1063 -0
  170. package/dist/store/display-id-alias.js.map +1 -0
  171. package/dist/store/dual-scope-db.d.ts.map +1 -1
  172. package/dist/store/dual-scope-db.js +21 -0
  173. package/dist/store/dual-scope-db.js.map +1 -1
  174. package/dist/store/exodus/reconcile.d.ts.map +1 -1
  175. package/dist/store/exodus/reconcile.js +7 -2
  176. package/dist/store/exodus/reconcile.js.map +1 -1
  177. package/dist/store/exodus/recovery.d.ts.map +1 -1
  178. package/dist/store/exodus/recovery.js +20 -1
  179. package/dist/store/exodus/recovery.js.map +1 -1
  180. package/dist/store/legacy-tasks-lineage.d.ts.map +1 -1
  181. package/dist/store/legacy-tasks-lineage.js +19 -2
  182. package/dist/store/legacy-tasks-lineage.js.map +1 -1
  183. package/dist/store/row-identity-flag.d.ts +26 -0
  184. package/dist/store/row-identity-flag.d.ts.map +1 -0
  185. package/dist/store/row-identity-flag.js +31 -0
  186. package/dist/store/row-identity-flag.js.map +1 -0
  187. package/dist/store/row-identity-registry.d.ts +61 -0
  188. package/dist/store/row-identity-registry.d.ts.map +1 -0
  189. package/dist/store/row-identity-registry.js +198 -0
  190. package/dist/store/row-identity-registry.js.map +1 -0
  191. package/dist/store/row-identity.d.ts +390 -0
  192. package/dist/store/row-identity.d.ts.map +1 -0
  193. package/dist/store/row-identity.js +1240 -0
  194. package/dist/store/row-identity.js.map +1 -0
  195. package/dist/store/safety-data-accessor.d.ts +2 -0
  196. package/dist/store/safety-data-accessor.d.ts.map +1 -1
  197. package/dist/store/safety-data-accessor.js +4 -0
  198. package/dist/store/safety-data-accessor.js.map +1 -1
  199. package/dist/store/schema/attachments.d.ts +1 -1
  200. package/dist/store/schema/audit.d.ts +4 -4
  201. package/dist/store/schema/background-jobs.d.ts +1 -1
  202. package/dist/store/schema/cleo-global/nexus.d.ts +1 -1
  203. package/dist/store/schema/cleo-global/skills.d.ts +2 -2
  204. package/dist/store/schema/cleo-project/audit.d.ts +4 -4
  205. package/dist/store/schema/cleo-project/docs.d.ts +1 -1
  206. package/dist/store/schema/cleo-project/lifecycle.d.ts +4 -4
  207. package/dist/store/schema/cleo-project/nexus-graph.d.ts +3 -3
  208. package/dist/store/schema/cleo-project/provenance-commits.d.ts +2 -2
  209. package/dist/store/schema/cleo-project/provenance-rest.d.ts +3 -3
  210. package/dist/store/schema/cleo-project/runtime.d.ts +3 -3
  211. package/dist/store/schema/cleo-project/tasks-core-batch2.d.ts +87 -1
  212. package/dist/store/schema/cleo-project/tasks-core-batch2.d.ts.map +1 -1
  213. package/dist/store/schema/cleo-project/tasks-core-batch2.js +35 -0
  214. package/dist/store/schema/cleo-project/tasks-core-batch2.js.map +1 -1
  215. package/dist/store/schema/cleo-project/tasks-core.d.ts +183 -7
  216. package/dist/store/schema/cleo-project/tasks-core.d.ts.map +1 -1
  217. package/dist/store/schema/cleo-project/tasks-core.js +52 -1
  218. package/dist/store/schema/cleo-project/tasks-core.js.map +1 -1
  219. package/dist/store/schema/cleo-shared/brain.d.ts +6 -6
  220. package/dist/store/schema/lifecycle.d.ts +4 -4
  221. package/dist/store/schema/manifest.d.ts +1 -1
  222. package/dist/store/schema/memory-schema.d.ts +5 -5
  223. package/dist/store/schema/nexus-schema.d.ts +3 -3
  224. package/dist/store/schema/provenance/releases.d.ts +1 -1
  225. package/dist/store/schema/skills-schema.d.ts +2 -2
  226. package/dist/store/schema/tasks.d.ts +6 -6
  227. package/dist/store/split-brain-import.d.ts +2 -14
  228. package/dist/store/split-brain-import.d.ts.map +1 -1
  229. package/dist/store/split-brain-import.js +4 -19
  230. package/dist/store/split-brain-import.js.map +1 -1
  231. package/dist/store/sqlite-data-accessor.d.ts +279 -2
  232. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  233. package/dist/store/sqlite-data-accessor.js +544 -44
  234. package/dist/store/sqlite-data-accessor.js.map +1 -1
  235. package/dist/store/sync/clock-store.d.ts +67 -0
  236. package/dist/store/sync/clock-store.d.ts.map +1 -0
  237. package/dist/store/sync/clock-store.js +117 -0
  238. package/dist/store/sync/clock-store.js.map +1 -0
  239. package/dist/store/sync/flags.d.ts +51 -0
  240. package/dist/store/sync/flags.d.ts.map +1 -0
  241. package/dist/store/sync/flags.js +81 -0
  242. package/dist/store/sync/flags.js.map +1 -0
  243. package/dist/store/sync/hlc.d.ts +115 -0
  244. package/dist/store/sync/hlc.d.ts.map +1 -0
  245. package/dist/store/sync/hlc.js +172 -0
  246. package/dist/store/sync/hlc.js.map +1 -0
  247. package/dist/store/sync/index.d.ts +20 -0
  248. package/dist/store/sync/index.d.ts.map +1 -0
  249. package/dist/store/sync/index.js +20 -0
  250. package/dist/store/sync/index.js.map +1 -0
  251. package/dist/store/sync/replica-registry.d.ts +85 -0
  252. package/dist/store/sync/replica-registry.d.ts.map +1 -0
  253. package/dist/store/sync/replica-registry.js +147 -0
  254. package/dist/store/sync/replica-registry.js.map +1 -0
  255. package/dist/store/sync/replica.d.ts +181 -0
  256. package/dist/store/sync/replica.d.ts.map +1 -0
  257. package/dist/store/sync/replica.js +305 -0
  258. package/dist/store/sync/replica.js.map +1 -0
  259. package/dist/store/sync/schema.d.ts +68 -0
  260. package/dist/store/sync/schema.d.ts.map +1 -0
  261. package/dist/store/sync/schema.js +130 -0
  262. package/dist/store/sync/schema.js.map +1 -0
  263. package/dist/store/sync/skew-hold.d.ts +71 -0
  264. package/dist/store/sync/skew-hold.d.ts.map +1 -0
  265. package/dist/store/sync/skew-hold.js +89 -0
  266. package/dist/store/sync/skew-hold.js.map +1 -0
  267. package/dist/store/table-classification.d.ts +10 -8
  268. package/dist/store/table-classification.d.ts.map +1 -1
  269. package/dist/store/table-classification.js +93 -25
  270. package/dist/store/table-classification.js.map +1 -1
  271. package/dist/store/tasks-sqlite.d.ts.map +1 -1
  272. package/dist/store/tasks-sqlite.js +9 -1
  273. package/dist/store/tasks-sqlite.js.map +1 -1
  274. package/dist/store/twin-collapse.d.ts +113 -2
  275. package/dist/store/twin-collapse.d.ts.map +1 -1
  276. package/dist/store/twin-collapse.js +247 -19
  277. package/dist/store/twin-collapse.js.map +1 -1
  278. package/dist/store/umbrella-data-accessor.d.ts +2 -0
  279. package/dist/store/umbrella-data-accessor.d.ts.map +1 -1
  280. package/dist/store/umbrella-data-accessor.js +4 -0
  281. package/dist/store/umbrella-data-accessor.js.map +1 -1
  282. package/dist/store/validation-schemas.d.ts +241 -49
  283. package/dist/store/validation-schemas.d.ts.map +1 -1
  284. package/dist/system/health.d.ts.map +1 -1
  285. package/dist/system/health.js +3 -0
  286. package/dist/system/health.js.map +1 -1
  287. package/dist/system/project-health.d.ts +9 -2
  288. package/dist/system/project-health.d.ts.map +1 -1
  289. package/dist/system/project-health.js +8 -3
  290. package/dist/system/project-health.js.map +1 -1
  291. package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
  292. package/dist/tasks/ac-coverage-gate.js +3 -0
  293. package/dist/tasks/ac-coverage-gate.js.map +1 -1
  294. package/dist/tasks/ac-table.d.ts +35 -7
  295. package/dist/tasks/ac-table.d.ts.map +1 -1
  296. package/dist/tasks/ac-table.js +119 -14
  297. package/dist/tasks/ac-table.js.map +1 -1
  298. package/dist/tasks/add.d.ts.map +1 -1
  299. package/dist/tasks/add.js +48 -2
  300. package/dist/tasks/add.js.map +1 -1
  301. package/dist/tasks/backfill-child-projections.js +47 -6
  302. package/dist/tasks/backfill-child-projections.js.map +1 -1
  303. package/dist/tasks/complete.d.ts.map +1 -1
  304. package/dist/tasks/complete.js +6 -1
  305. package/dist/tasks/complete.js.map +1 -1
  306. package/dist/tasks/completion-evaluation.d.ts.map +1 -1
  307. package/dist/tasks/completion-evaluation.js +3 -0
  308. package/dist/tasks/completion-evaluation.js.map +1 -1
  309. package/dist/tasks/evidence.d.ts.map +1 -1
  310. package/dist/tasks/evidence.js +14 -3
  311. package/dist/tasks/evidence.js.map +1 -1
  312. package/dist/validation/compliance.d.ts +5 -1
  313. package/dist/validation/compliance.d.ts.map +1 -1
  314. package/dist/validation/compliance.js +24 -3
  315. package/dist/validation/compliance.js.map +1 -1
  316. package/dist/validation/index.d.ts +1 -1
  317. package/dist/validation/index.d.ts.map +1 -1
  318. package/dist/validation/index.js +1 -1
  319. package/dist/validation/index.js.map +1 -1
  320. package/dist/validation/protocol-common.d.ts +47 -2
  321. package/dist/validation/protocol-common.d.ts.map +1 -1
  322. package/dist/validation/protocol-common.js +86 -21
  323. package/dist/validation/protocol-common.js.map +1 -1
  324. package/migrations/README.md +12 -0
  325. package/migrations/drizzle-cleo-project/20260928120000_t12341-row-uids/migration.sql +169 -0
  326. package/migrations/sync-journal/20260929140000_t12342-sync-clock/migration.sql +42 -0
  327. package/package.json +13 -13
  328. package/templates/CLEO-REFERENCE.md +4 -0
@@ -0,0 +1,1063 @@
1
+ /**
2
+ * Display ids, uid re-keys, their aliases, and the identity quarantine (T12341).
3
+ *
4
+ * `T####` is a per-project DISPLAY id: unique inside one store at rest, typed
5
+ * by people and agents, but allocated locally, so two offline stores can
6
+ * allocate the same one for different work. The row's uid is its identity
7
+ * (`store/row-identity.ts`). Spec: `cleo docs fetch t12341-uid-scheme` §6.4, §9.
8
+ *
9
+ * ## Receiving a row: place it, or hold it
10
+ *
11
+ * An incoming row travels as a {@link WireRow}: its uid, its birth
12
+ * fingerprint, its values, and every reference as the referenced row's
13
+ * (uid, birth fingerprint) ({@link WireRef}). {@link receiveRow} places it in
14
+ * its table, or HOLDS it in `tasks_identity_quarantine` (local-only) when it
15
+ * cannot be placed yet:
16
+ *
17
+ * - `uid-collision`: a local row has its uid with a different birth
18
+ * fingerprint. Neither row is re-keyed here unless this replica is the
19
+ * authority ({@link rekeyRowUid}); the others wait for the published
20
+ * {@link RekeyReceipt} ({@link applyRekey}).
21
+ * - `display-id-collision`: a local row holds its display id. The loser is
22
+ * re-numbered only by its authority ({@link remintTaskDisplayId}); the
23
+ * others wait for the {@link RemintOp} ({@link applyRemintOp}).
24
+ * - `key-collision`: another local row holds its local key (a key derived
25
+ * from a local display id, such as an AC id); the merge engine re-derives
26
+ * the key (T12344).
27
+ * - `ref-pending`: a reference does not resolve to a local row with the SAME
28
+ * birth fingerprint ({@link resolveWireRef}), e.g. it names the loser of a
29
+ * uid collision whose re-key has not arrived. It never resolves to the
30
+ * other row that shares the uid.
31
+ *
32
+ * Held rows never enter the task tables, so no validator, parser or command
33
+ * ever sees an id that is not a `T####` (the round-1 provisional `prov-` ids
34
+ * are gone). Every op that could unblock a held row re-tries the held rows
35
+ * ({@link releaseHeld}).
36
+ *
37
+ * ## Display-id collision: one authority, ordered by HLC
38
+ *
39
+ * Every replica agrees on the loser ({@link collisionLoser}: the greater
40
+ * uid). {@link remintAuthority} is a pure function of synced inputs (cloud
41
+ * sync, the origin, replica membership with join / retire HLCs, the
42
+ * collision's HLC) and a point in HLC time: the origin first, then each
43
+ * active replica in turn for one takeover window, cycling, so a silent
44
+ * authority is always taken over and nothing waits forever. Two authorities
45
+ * may both re-mint around a window boundary: {@link applyRemintOp} keeps the
46
+ * op with the greater HLC on every replica, whatever the arrival order, and
47
+ * the other number becomes an alias.
48
+ *
49
+ * ## Uid collision: re-key the loser
50
+ *
51
+ * The loser is the greater birth fingerprint. {@link rekeyRowUid} asserts the
52
+ * row it re-keys carries that fingerprint (a replica holding the winner never
53
+ * re-keys it), gives it a random uid, and cascades from STORED identity:
54
+ * minted children get {@link rekeyedChildUid} of their old uid and the new
55
+ * owner uid, natural rows keyed by a re-keyed row are re-derived, stored uid
56
+ * copies (`ac_uid`) and display-id aliases follow. The receipt lists every
57
+ * value; receivers apply them ({@link applyRekey}) and never recompute.
58
+ *
59
+ * ## Gate 28
60
+ *
61
+ * Every write here goes through a chokepoint helper in
62
+ * `sqlite-data-accessor.ts`; this module only reads and decides.
63
+ *
64
+ * @module
65
+ * @task T12341
66
+ * @task T12744
67
+ * @task T12745
68
+ * @task T12748
69
+ * @task T12750
70
+ * @epic T12323
71
+ */
72
+ import { ExitCode } from '@cleocode/contracts';
73
+ import { CleoError } from '../errors.js';
74
+ import { BIRTH_FP_COLUMN, fillTableUids, markRowIdentityShared, mintRowUid, naturalRowUid, ROW_IDENTITY, ROW_IDENTITY_META_TABLE, registerRowUidFunction, rekeyedChildUid, rowIdentitySpec, UID_COLUMN, } from './row-identity.js';
75
+ import { advanceTaskIdSequence, clearRowUidNative, deleteDisplayIdAliasNative, deleteQuarantineNative, deleteRowIdentityMetaNative, insertDisplayIdAliasNative, insertIdentityRowNative, insertQuarantineNative, insertUidAliasNative, rekeyQuarantineNative, renameTaskDisplayIdNative, rewriteStoredRefUidNative, setNaturalUidNative, setRowUidNative, writeRowIdentityMetaNative, } from './sqlite-data-accessor.js';
76
+ /** Physical name of the display-id alias table. */
77
+ export const DISPLAY_ID_ALIAS_TABLE = 'tasks_display_id_aliases';
78
+ /** Physical name of the uid re-key alias table. */
79
+ export const UID_ALIAS_TABLE = 'tasks_uid_aliases';
80
+ /** Physical name of the identity quarantine (local-only). */
81
+ export const QUARANTINE_TABLE = 'tasks_identity_quarantine';
82
+ const HLC_RE = /^(\d{15})\.(\d{6})\.(.+)$/;
83
+ /**
84
+ * Encode an HLC value.
85
+ *
86
+ * @param hlc - The value.
87
+ * @returns Its sortable string form.
88
+ */
89
+ export function encodeHlc(hlc) {
90
+ return `${String(hlc.physicalMs).padStart(15, '0')}.${String(hlc.counter).padStart(6, '0')}.${hlc.node}`;
91
+ }
92
+ /**
93
+ * Parse an encoded HLC value.
94
+ *
95
+ * @param value - Encoded HLC.
96
+ * @returns The value.
97
+ * @throws Error when the value is not an encoded HLC.
98
+ */
99
+ export function parseHlc(value) {
100
+ const m = HLC_RE.exec(value);
101
+ if (!m)
102
+ throw new Error(`not an HLC value: ${value}`);
103
+ return { physicalMs: Number(m[1]), counter: Number(m[2]), node: m[3] };
104
+ }
105
+ /**
106
+ * Total order of two encoded HLC values.
107
+ *
108
+ * @param a - One value.
109
+ * @param b - The other.
110
+ * @returns Negative, zero or positive.
111
+ */
112
+ export function compareHlc(a, b) {
113
+ const x = parseHlc(a);
114
+ const y = parseHlc(b);
115
+ if (x.physicalMs !== y.physicalMs)
116
+ return x.physicalMs < y.physicalMs ? -1 : 1;
117
+ if (x.counter !== y.counter)
118
+ return x.counter < y.counter ? -1 : 1;
119
+ return x.node < y.node ? -1 : x.node > y.node ? 1 : 0;
120
+ }
121
+ // ---- Shared helpers ---------------------------------------------------------
122
+ /** Quote an identifier for SQL. */
123
+ function q(name) {
124
+ return `"${name.replaceAll('"', '""')}"`;
125
+ }
126
+ /** The declared project spec of `table`, or throw. */
127
+ function specOf(table) {
128
+ const spec = rowIdentitySpec('project', table);
129
+ if (!spec)
130
+ throw new Error(`row identity: ${table} is not a declared project table`);
131
+ return spec;
132
+ }
133
+ /** The single-column key of a declared table. */
134
+ function keyOf(table) {
135
+ const spec = specOf(table);
136
+ if (spec.key.length !== 1)
137
+ throw new Error(`row identity: ${table} has a composite key`);
138
+ return spec.key[0];
139
+ }
140
+ /** Run `fn` in a uniquely named savepoint. */
141
+ function inSavepoint(db, name, fn) {
142
+ const sp = `_cleo_${name}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
143
+ db.exec(`SAVEPOINT ${sp}`);
144
+ try {
145
+ const out = fn();
146
+ db.exec(`RELEASE SAVEPOINT ${sp}`);
147
+ return out;
148
+ }
149
+ catch (error) {
150
+ db.exec(`ROLLBACK TO SAVEPOINT ${sp}`);
151
+ db.exec(`RELEASE SAVEPOINT ${sp}`);
152
+ throw error;
153
+ }
154
+ }
155
+ /** A row-identity meta value. */
156
+ function readMeta(db, key) {
157
+ const row = db
158
+ .prepare(`SELECT value FROM main.${q(ROW_IDENTITY_META_TABLE)} WHERE key = ?`)
159
+ .get(key);
160
+ return row?.value;
161
+ }
162
+ /** The local row of a minted table with this uid AND birth fingerprint. */
163
+ function localRow(db, table, uid, birthFp) {
164
+ return db
165
+ .prepare(`SELECT ${q(keyOf(table))} AS key, rowid AS rowid FROM main.${q(table)}
166
+ WHERE ${q(UID_COLUMN)} = ? AND ${q(BIRTH_FP_COLUMN)} = ?`)
167
+ .get(uid, birthFp);
168
+ }
169
+ // ---- Display-id collision: loser and authority -------------------------------
170
+ /**
171
+ * Of two rows that carry the same display id, the one that must be re-minted:
172
+ * the greater uid. Every replica picks the same one for the same pair, and for
173
+ * UUIDv7 uids the older row keeps its id.
174
+ *
175
+ * @param a - Uid of one claimant.
176
+ * @param b - Uid of the other.
177
+ * @returns The uid to re-mint.
178
+ */
179
+ export function collisionLoser(a, b) {
180
+ return a.toLowerCase() > b.toLowerCase() ? a : b;
181
+ }
182
+ /** Default length of one authority window (HLC physical time). */
183
+ export const REMINT_TAKEOVER_MS = 72 * 60 * 60 * 1000;
184
+ /**
185
+ * Which replica may re-mint the losing row of a display-id collision at HLC
186
+ * time `atHlc` (spec §9.2). A pure function of synced inputs: every replica
187
+ * evaluating the same time names the same authority.
188
+ *
189
+ * 1. The project syncs to cloud → the server.
190
+ * 2. Else the origin, while it is an active member, for the first window
191
+ * after the collision.
192
+ * 3. Else the FALLBACK CHAIN: the active members other than the origin,
193
+ * sorted, one window each, cycling. A fallback that stays silent loses its
194
+ * turn when its window ends, so a collision never waits forever.
195
+ *
196
+ * @param input - The facts every replica can see.
197
+ * @returns The authority, the rule, and the window index (0 = origin).
198
+ */
199
+ export function remintAuthority(input) {
200
+ if (input.cloudSynced)
201
+ return { authority: 'server', reason: 'server', slot: 0 };
202
+ const window = input.takeoverAfterMs ?? REMINT_TAKEOVER_MS;
203
+ const elapsed = Math.max(0, parseHlc(input.atHlc).physicalMs - parseHlc(input.collisionHlc).physicalMs);
204
+ const active = input.replicas
205
+ .filter((r) => compareHlc(r.joinedHlc, input.atHlc) <= 0 &&
206
+ (r.retiredHlc == null || compareHlc(r.retiredHlc, input.atHlc) > 0))
207
+ .map((r) => r.id)
208
+ .sort();
209
+ if (active.length === 0)
210
+ throw new Error('remint authority: the project has no active replica');
211
+ const originLive = input.origin !== null && active.includes(input.origin);
212
+ if (originLive && elapsed < window) {
213
+ return { authority: input.origin, reason: 'origin', slot: 0 };
214
+ }
215
+ const others = active.filter((id) => id !== input.origin);
216
+ const chain = others.length > 0 ? others : active;
217
+ const turn = Math.floor(elapsed / window) - (originLive ? 1 : 0);
218
+ return {
219
+ authority: chain[turn % chain.length],
220
+ reason: 'fallback',
221
+ slot: turn + 1,
222
+ };
223
+ }
224
+ /**
225
+ * Record that `displayId` used to name the row (`entityUid`, `entityBirthFp`).
226
+ * Idempotent.
227
+ *
228
+ * @param db - Connection on the project `cleo.db`.
229
+ * @param entry - The displaced id, the row that carried it, and the displacement.
230
+ */
231
+ export function recordDisplayIdAlias(db, entry) {
232
+ insertDisplayIdAliasNative(db, {
233
+ uid: naturalRowUid('project', DISPLAY_ID_ALIAS_TABLE, [
234
+ entry.table,
235
+ entry.displayId,
236
+ entry.entityUid,
237
+ ]),
238
+ entityTable: entry.table,
239
+ displayId: entry.displayId,
240
+ entityUid: entry.entityUid,
241
+ entityBirthFp: entry.entityBirthFp,
242
+ reason: entry.reason,
243
+ origin: entry.origin ?? null,
244
+ displacedHlc: entry.displacedHlc ?? null,
245
+ createdAt: entry.now ?? new Date().toISOString(),
246
+ });
247
+ }
248
+ /**
249
+ * Resolve a display id: a live row always wins (aliases come back as
250
+ * history); with no live row, one alias resolves and several are ambiguous.
251
+ *
252
+ * @param db - Connection on the project `cleo.db`.
253
+ * @param table - Display-id table (e.g. `tasks_tasks`).
254
+ * @param displayId - The id to resolve.
255
+ * @returns `resolved` (live, or the single alias), `ambiguous` with every
256
+ * candidate (callers report it as an error and never pick), or `none`.
257
+ */
258
+ export function resolveDisplayId(db, table, displayId) {
259
+ const spec = specOf(table);
260
+ if (!spec.displayId)
261
+ throw new Error(`display id: ${table} has no declared display id`);
262
+ const key = keyOf(table);
263
+ const live = db
264
+ .prepare(`SELECT ${q(UID_COLUMN)} AS uid FROM main.${q(table)} WHERE ${q(key)} = ?`)
265
+ .get(displayId);
266
+ const aliases = db
267
+ .prepare(`SELECT a.entity_uid AS uid, t.${q(key)} AS currentId, a.origin AS origin,
268
+ a.displaced_hlc AS displacedHlc
269
+ FROM ${DISPLAY_ID_ALIAS_TABLE} a
270
+ LEFT JOIN main.${q(table)} t
271
+ ON t.${q(UID_COLUMN)} = a.entity_uid
272
+ AND (a.entity_birth_fp IS NULL OR t.${q(BIRTH_FP_COLUMN)} = a.entity_birth_fp)
273
+ WHERE a.entity_table = ? AND a.display_id = ?
274
+ ORDER BY a.displaced_hlc, a.entity_uid`)
275
+ .all(table, displayId)
276
+ .filter((a) => a.uid !== live?.uid)
277
+ .map((a) => ({ ...a, via: 'alias' }));
278
+ if (live) {
279
+ return {
280
+ status: 'resolved',
281
+ claimant: { uid: live.uid, currentId: displayId, via: 'live' },
282
+ alsoKnownAs: aliases,
283
+ };
284
+ }
285
+ const [only] = aliases;
286
+ if (!only)
287
+ return { status: 'none' };
288
+ if (aliases.length > 1)
289
+ return { status: 'ambiguous', candidates: aliases };
290
+ return { status: 'resolved', claimant: only, alsoKnownAs: [] };
291
+ }
292
+ /**
293
+ * Allocate the next `T####` on the caller's handle, with the same lower bound
294
+ * `allocateNextTaskId` uses (the highest stored numeric id). Only the re-mint
295
+ * authority allocates for a collision.
296
+ *
297
+ * @param db - Connection on the project `cleo.db`.
298
+ * @returns The new display id.
299
+ */
300
+ export function allocateTaskDisplayId(db) {
301
+ const inventory = db
302
+ .prepare(`SELECT COALESCE(MAX(CAST(substr(id, 2) AS INTEGER)), 0) AS maximum
303
+ FROM tasks_tasks WHERE id GLOB 'T[0-9]*' AND substr(id, 2) NOT GLOB '*[^0-9]*'`)
304
+ .get();
305
+ const counter = advanceTaskIdSequence(db, inventory.maximum);
306
+ if (counter === undefined) {
307
+ throw new CleoError(ExitCode.FILE_ERROR, 'Sequence counter not found during allocation', {
308
+ fix: 'Run `cleo sequence repair` to re-seed the task-id sequence, then retry.',
309
+ });
310
+ }
311
+ return `T${String(counter).padStart(3, '0')}`;
312
+ }
313
+ /** Every local column that holds a task's display id. */
314
+ function taskReferenceColumns(db) {
315
+ const found = new Map();
316
+ const add = (table, column, jsonArray) => {
317
+ found.set(`${table}.${column}`, { table, column, jsonArray });
318
+ };
319
+ const fks = db
320
+ .prepare(`SELECT m.name AS tbl, f."from" AS col
321
+ FROM main.sqlite_master m, pragma_foreign_key_list(m.name) f
322
+ WHERE m.type = 'table' AND f."table" = 'tasks_tasks'
323
+ AND (f."to" IS NULL OR f."to" = 'id')`)
324
+ .all();
325
+ for (const fk of fks)
326
+ add(fk.tbl, fk.col, false);
327
+ for (const spec of ROW_IDENTITY.project) {
328
+ for (const ref of [...(spec.refs ?? []), ...(spec.keyRefs ?? []), ...(spec.owners ?? [])]) {
329
+ if (ref.table === 'tasks_tasks')
330
+ add(spec.table, ref.column, false);
331
+ }
332
+ for (const ref of spec.jsonArrayRefs ?? []) {
333
+ if (ref.table === 'tasks_tasks')
334
+ add(spec.table, ref.column, true);
335
+ }
336
+ }
337
+ const tables = new Set(db.prepare("SELECT name FROM main.sqlite_master WHERE type = 'table'").all().map((r) => r.name));
338
+ return [...found.values()].filter((ref) => tables.has(ref.table));
339
+ }
340
+ function remintKey(uid, birthFp) {
341
+ return `remint:tasks_tasks:${uid}:${birthFp}`;
342
+ }
343
+ function readRemint(db, uid, birthFp) {
344
+ const raw = readMeta(db, remintKey(uid, birthFp));
345
+ return raw === undefined ? undefined : JSON.parse(raw);
346
+ }
347
+ function writeRemint(db, op) {
348
+ writeRowIdentityMetaNative(db, remintKey(op.uid, op.birthFp), JSON.stringify({ newId: op.newId, hlc: op.hlc, origin: op.origin }));
349
+ }
350
+ /**
351
+ * AUTHORITY ONLY ({@link remintAuthority}): give the losing task of a
352
+ * display-id collision a new display id. Allocates the next `T####`, renames
353
+ * through the chokepoint (version CAS, claim columns kept), records the
354
+ * contested id as an alias and the op as the row's re-mint record, places any
355
+ * held row the freed id unblocks, and returns the op to publish.
356
+ *
357
+ * @param db - Connection on the project `cleo.db`.
358
+ * @param taskId - Current local display id of the task to re-mint.
359
+ * @param options - The contested id (default: `taskId`), why, the authority,
360
+ * the op's HLC, and an optional version / claim guard for a manual rename.
361
+ * @returns The op to publish, with what was rewritten here.
362
+ * @throws CleoError NOT_FOUND when the task does not exist or has no identity yet.
363
+ */
364
+ export function remintTaskDisplayId(db, taskId, options) {
365
+ const row = db
366
+ .prepare('SELECT uid, birth_fp AS birthFp FROM tasks_tasks WHERE id = ?')
367
+ .get(taskId);
368
+ if (!row?.uid || !row.birthFp) {
369
+ throw new CleoError(ExitCode.NOT_FOUND, `No task ${taskId} with a uid and birth fingerprint`, {
370
+ fix: 'Check the id with `cleo show`; row identity is filled when CLEO_ROW_UID_FILL=1.',
371
+ details: { field: 'taskId', actual: taskId },
372
+ });
373
+ }
374
+ const { uid, birthFp } = row;
375
+ const oldId = options.contestedId ?? taskId;
376
+ registerRowUidFunction(db, 'project');
377
+ return inSavepoint(db, 'remint', () => {
378
+ const newId = allocateTaskDisplayId(db);
379
+ const { rewritten } = renameTaskDisplayIdNative(db, uid, newId, taskReferenceColumns(db), options.guard);
380
+ recordDisplayIdAlias(db, {
381
+ table: 'tasks_tasks',
382
+ displayId: oldId,
383
+ entityUid: uid,
384
+ entityBirthFp: birthFp,
385
+ reason: options.reason,
386
+ origin: options.origin,
387
+ displacedHlc: options.hlc,
388
+ now: options.now,
389
+ });
390
+ const op = { uid, birthFp, oldId, newId, origin: options.origin, hlc: options.hlc };
391
+ writeRemint(db, op);
392
+ return { ...op, rewritten, released: releaseHeldRows(db) };
393
+ });
394
+ }
395
+ /**
396
+ * NON-AUTHORITY: apply a re-mint an authority published. Of several ops for
397
+ * one row, the one with the greatest HLC wins on every replica, whatever the
398
+ * arrival order: a lesser op is recorded as an alias (`superseded`), a greater
399
+ * one renames the row again and turns the previous number into an alias.
400
+ * Never allocates.
401
+ *
402
+ * @param db - Connection on the project `cleo.db`.
403
+ * @param op - The published re-mint.
404
+ * @returns What happened.
405
+ */
406
+ export function applyRemintOp(db, op) {
407
+ registerRowUidFunction(db, 'project');
408
+ markRowIdentityShared(db, 'receive');
409
+ const prior = readRemint(db, op.uid, op.birthFp);
410
+ const alias = (displayId, reason) => recordDisplayIdAlias(db, {
411
+ table: 'tasks_tasks',
412
+ displayId,
413
+ entityUid: op.uid,
414
+ entityBirthFp: op.birthFp,
415
+ reason,
416
+ origin: op.origin,
417
+ displacedHlc: op.hlc,
418
+ });
419
+ if (prior) {
420
+ const order = compareHlc(op.hlc, prior.hlc);
421
+ if (order === 0)
422
+ return { status: 'already-applied' };
423
+ if (order < 0) {
424
+ inSavepoint(db, 'remint_superseded', () => {
425
+ alias(op.oldId, 'collision-remint');
426
+ if (op.newId !== prior.newId)
427
+ alias(op.newId, 'superseded-remint');
428
+ });
429
+ return { status: 'superseded', winner: prior };
430
+ }
431
+ }
432
+ const live = localRow(db, 'tasks_tasks', op.uid, op.birthFp);
433
+ if (live) {
434
+ const holder = db.prepare('SELECT uid FROM tasks_tasks WHERE id = ?').get(op.newId);
435
+ if (holder && holder.uid !== op.uid)
436
+ return { status: 'conflict', holderUid: holder.uid };
437
+ }
438
+ return inSavepoint(db, 'remint_apply', () => {
439
+ alias(op.oldId, 'collision-remint');
440
+ if (prior && prior.newId !== op.newId)
441
+ alias(prior.newId, 'superseded-remint');
442
+ writeRemint(db, op);
443
+ if (!live)
444
+ return { status: 'recorded', released: releaseHeldRows(db) };
445
+ if (live.key === op.newId)
446
+ return { status: 'already-applied' };
447
+ const { rewritten } = renameTaskDisplayIdNative(db, op.uid, op.newId, taskReferenceColumns(db));
448
+ return {
449
+ status: 'applied',
450
+ receipt: { ...op, rewritten, released: releaseHeldRows(db) },
451
+ };
452
+ });
453
+ }
454
+ /** Follow `tasks_uid_aliases` from (uid, birth fp) to the row's current uid. */
455
+ function followUidAlias(db, table, uid, birthFp) {
456
+ let current = uid;
457
+ for (let hop = 0; hop < 32; hop++) {
458
+ const alias = db
459
+ .prepare(`SELECT new_uid AS uid FROM ${UID_ALIAS_TABLE}
460
+ WHERE entity_table = ? AND old_uid = ? AND old_birth_fp = ?`)
461
+ .get(table, current, birthFp);
462
+ if (!alias)
463
+ break;
464
+ current = alias.uid;
465
+ }
466
+ return current;
467
+ }
468
+ /**
469
+ * Resolve a wire reference to a local row: the row with that uid AND that
470
+ * birth fingerprint, following `tasks_uid_aliases` when the target was
471
+ * re-keyed. A row that has the uid with ANOTHER fingerprint (the other side of
472
+ * a uid collision) never matches: the reference is `held` (its target waits
473
+ * in the quarantine) or `pending` (its target has not arrived, or its re-key
474
+ * has not), and the referencing row waits too.
475
+ *
476
+ * @param db - Connection on the project `cleo.db`.
477
+ * @param table - The referenced table.
478
+ * @param ref - The reference.
479
+ * @returns Where it points.
480
+ */
481
+ export function resolveWireRef(db, table, ref) {
482
+ const minted = specOf(table).kind === 'minted';
483
+ const uid = minted && ref.birthFp !== null ? followUidAlias(db, table, ref.uid, ref.birthFp) : ref.uid;
484
+ const row = db
485
+ .prepare(`SELECT ${q(keyOf(table))} AS key, ${minted ? q(BIRTH_FP_COLUMN) : 'NULL'} AS fp
486
+ FROM main.${q(table)} WHERE ${q(UID_COLUMN)} = ?`)
487
+ .get(uid);
488
+ if (row && (!minted || ref.birthFp === null || row.fp === ref.birthFp)) {
489
+ return { status: 'row', key: row.key, uid };
490
+ }
491
+ const held = db
492
+ .prepare(`SELECT 1 AS x FROM ${QUARANTINE_TABLE} WHERE entity_table = ? AND uid = ? AND birth_fp = ?`)
493
+ .get(table, uid, ref.birthFp ?? '');
494
+ return held ? { status: 'held' } : { status: 'pending' };
495
+ }
496
+ /** Reference column → referenced table, for a declared table. */
497
+ function refTargets(spec) {
498
+ const out = new Map();
499
+ for (const ref of [...(spec.refs ?? []), ...(spec.owners ?? []), ...(spec.keyRefs ?? [])]) {
500
+ out.set(ref.column, ref.table);
501
+ }
502
+ return out;
503
+ }
504
+ /** Hold a row, keeping the time it was first held. */
505
+ function hold(db, wire, reason, contestedId, previous) {
506
+ const key = { entityTable: wire.table, uid: wire.uid, birthFp: wire.birthFp ?? '' };
507
+ const was = previous ?? key;
508
+ const first = db
509
+ .prepare(`SELECT created_at AS createdAt FROM ${QUARANTINE_TABLE}
510
+ WHERE entity_table = ? AND uid = ? AND birth_fp = ?`)
511
+ .get(was.entityTable, was.uid, was.birthFp);
512
+ if (previous)
513
+ deleteQuarantineNative(db, previous);
514
+ insertQuarantineNative(db, {
515
+ ...key,
516
+ reason,
517
+ contestedId,
518
+ rowJson: JSON.stringify(wire),
519
+ receivedHlc: null,
520
+ createdAt: first?.createdAt ?? new Date().toISOString(),
521
+ });
522
+ }
523
+ /** Place one wire row, or hold it. Does not re-try other held rows. */
524
+ function placeRow(db, incoming, heldAs) {
525
+ const spec = specOf(incoming.table);
526
+ const minted = spec.kind === 'minted';
527
+ if (minted && !incoming.birthFp) {
528
+ throw new Error(`receive: a ${incoming.table} row must carry its birth fingerprint`);
529
+ }
530
+ // A late copy of a row that was re-keyed: follow its uid alias.
531
+ const wire = minted
532
+ ? {
533
+ ...incoming,
534
+ uid: followUidAlias(db, incoming.table, incoming.uid, incoming.birthFp),
535
+ }
536
+ : incoming;
537
+ const done = () => {
538
+ if (heldAs)
539
+ deleteQuarantineNative(db, heldAs);
540
+ };
541
+ // A natural row is matched by its key AFTER its references resolve (below):
542
+ // its wire uid hashes endpoint uids, and an endpoint under a uid collision
543
+ // would make two different edges look like one.
544
+ const same = minted
545
+ ? db
546
+ .prepare(`SELECT ${q(BIRTH_FP_COLUMN)} AS fp FROM main.${q(wire.table)} WHERE ${q(UID_COLUMN)} = ?`)
547
+ .get(wire.uid)
548
+ : undefined;
549
+ if (same) {
550
+ if (same.fp === wire.birthFp) {
551
+ done();
552
+ return { status: 'duplicate', uid: wire.uid };
553
+ }
554
+ const local = same.fp ?? '';
555
+ const other = wire.birthFp;
556
+ const localIsLoser = local > other;
557
+ hold(db, wire, 'uid-collision', null, heldAs);
558
+ return {
559
+ status: 'held',
560
+ reason: 'uid-collision',
561
+ collision: {
562
+ kind: 'uid',
563
+ table: wire.table,
564
+ uid: wire.uid,
565
+ winnerBirthFp: localIsLoser ? other : local,
566
+ loserBirthFp: localIsLoser ? local : other,
567
+ localIsLoser,
568
+ },
569
+ };
570
+ }
571
+ const values = { ...wire.values };
572
+ const targets = refTargets(spec);
573
+ for (const [column, ref] of Object.entries(wire.refs ?? {})) {
574
+ const target = targets.get(column);
575
+ if (!target)
576
+ throw new Error(`receive: ${wire.table}.${column} is not a declared reference`);
577
+ if (ref === null) {
578
+ values[column] = null;
579
+ continue;
580
+ }
581
+ const resolved = resolveWireRef(db, target, ref);
582
+ if (resolved.status !== 'row') {
583
+ hold(db, wire, 'ref-pending', null, heldAs);
584
+ return { status: 'held', reason: 'ref-pending' };
585
+ }
586
+ values[column] = resolved.key;
587
+ }
588
+ if (spec.displayId && minted) {
589
+ const key = keyOf(wire.table);
590
+ const remint = readRemint(db, wire.uid, wire.birthFp);
591
+ if (remint)
592
+ values[key] = remint.newId;
593
+ const holder = db
594
+ .prepare(`SELECT ${q(UID_COLUMN)} AS uid FROM main.${q(wire.table)} WHERE ${q(key)} = ?`)
595
+ .get(values[key] ?? null);
596
+ if (holder) {
597
+ const contested = String(values[key]);
598
+ const loserUid = collisionLoser(holder.uid ?? '', wire.uid);
599
+ hold(db, wire, 'display-id-collision', contested, heldAs);
600
+ return {
601
+ status: 'held',
602
+ reason: 'display-id-collision',
603
+ collision: {
604
+ kind: 'display-id',
605
+ table: wire.table,
606
+ displayId: contested,
607
+ loserUid,
608
+ localIsLoser: loserUid === holder.uid,
609
+ },
610
+ };
611
+ }
612
+ }
613
+ if (!minted) {
614
+ const existing = db
615
+ .prepare(`SELECT ${q(UID_COLUMN)} AS uid FROM main.${q(wire.table)}
616
+ WHERE ${spec.key.map((k) => `${q(k)} = ?`).join(' AND ')}`)
617
+ .get(...spec.key.map((k) => values[k] ?? null));
618
+ if (existing) {
619
+ done();
620
+ return { status: 'duplicate', uid: existing.uid ?? wire.uid };
621
+ }
622
+ }
623
+ const rowidKey = rowidKeyOf(db, wire.table);
624
+ // An INTEGER PRIMARY KEY (AUTOINCREMENT) numbers from 1 on every device: the
625
+ // sender's value means nothing here. Drop it; SQLite assigns a local one (T12799).
626
+ if (rowidKey)
627
+ delete values[rowidKey];
628
+ if (!spec.displayId && spec.key.length === 1 && !rowidKey) {
629
+ // A local key another row holds (keys such as AC ids derive from local
630
+ // display ids): the merge engine re-derives it (T12344); held until then.
631
+ const key = spec.key[0];
632
+ const taken = db
633
+ .prepare(`SELECT 1 AS x FROM main.${q(wire.table)} WHERE ${q(key)} = ?`)
634
+ .get(values[key] ?? null);
635
+ if (taken) {
636
+ hold(db, wire, 'key-collision', String(values[key]), heldAs);
637
+ return { status: 'held', reason: 'key-collision' };
638
+ }
639
+ }
640
+ if (minted) {
641
+ insertIdentityRowNative(db, wire.table, {
642
+ ...values,
643
+ [UID_COLUMN]: wire.uid,
644
+ [BIRTH_FP_COLUMN]: wire.birthFp,
645
+ });
646
+ }
647
+ else {
648
+ // A natural uid is a pure function of its endpoints: derive it here.
649
+ insertIdentityRowNative(db, wire.table, { ...values, [UID_COLUMN]: null });
650
+ fillTableUids(db, 'project', wire.table);
651
+ }
652
+ done();
653
+ const key = rowidKey
654
+ ? String(db
655
+ .prepare(`SELECT ${q(rowidKey)} AS k FROM main.${q(wire.table)} WHERE ${q(UID_COLUMN)} = ?`)
656
+ .get(wire.uid).k)
657
+ : spec.key.length === 1
658
+ ? String(values[spec.key[0]])
659
+ : wire.uid;
660
+ return { status: 'inserted', key, uid: wire.uid };
661
+ }
662
+ /** The table's INTEGER PRIMARY KEY column (a rowid alias, locally numbered), if it has one. */
663
+ function rowidKeyOf(db, table) {
664
+ const pk = db.prepare('SELECT name, type, pk FROM pragma_table_info(?)').all(table).filter((c) => c.pk > 0);
665
+ const [only] = pk;
666
+ return pk.length === 1 && only && only.type.toUpperCase() === 'INTEGER' ? only.name : undefined;
667
+ }
668
+ /**
669
+ * Re-try every held row until none moves: a placement can unblock others
670
+ * (a released owner unblocks its children).
671
+ */
672
+ function releaseHeldRows(db) {
673
+ const released = [];
674
+ for (let round = 0; round < 64; round++) {
675
+ const held = db
676
+ .prepare(`SELECT entity_table AS entityTable, uid, birth_fp AS birthFp, row_json AS rowJson
677
+ FROM ${QUARANTINE_TABLE} ORDER BY created_at, entity_table, uid`)
678
+ .all();
679
+ let moved = false;
680
+ for (const h of held) {
681
+ const key = { entityTable: h.entityTable, uid: h.uid, birthFp: h.birthFp };
682
+ const result = placeRow(db, JSON.parse(h.rowJson), key);
683
+ if (result.status !== 'held') {
684
+ released.push(key);
685
+ moved = true;
686
+ }
687
+ }
688
+ if (!moved)
689
+ break;
690
+ }
691
+ return released;
692
+ }
693
+ /**
694
+ * Receive one row: place it, or hold it (module docs). The first receive
695
+ * marks the store's identity values as shared, so the open pass never
696
+ * clears them (T12746).
697
+ *
698
+ * @param db - Connection on the project `cleo.db`, inside the merge transaction.
699
+ * @param wire - The incoming row.
700
+ * @returns What happened; a `held` result carries the collision for the caller
701
+ * (the merge engine) to hand to its authority.
702
+ */
703
+ export function receiveRow(db, wire) {
704
+ registerRowUidFunction(db, 'project');
705
+ return inSavepoint(db, 'receive', () => {
706
+ markRowIdentityShared(db, 'receive');
707
+ const result = placeRow(db, wire);
708
+ if (result.status === 'inserted')
709
+ releaseHeldRows(db);
710
+ return result;
711
+ });
712
+ }
713
+ /**
714
+ * Re-try the held rows now (after an op this module did not apply).
715
+ *
716
+ * @param db - Connection on the project `cleo.db`.
717
+ * @returns The rows placed.
718
+ */
719
+ export function releaseHeld(db) {
720
+ registerRowUidFunction(db, 'project');
721
+ return inSavepoint(db, 'release', () => releaseHeldRows(db));
722
+ }
723
+ /**
724
+ * Every held row (read-only: held rows are never edited in place).
725
+ *
726
+ * @param db - Connection on the project `cleo.db`.
727
+ * @returns The held rows, oldest first.
728
+ */
729
+ export function listHeldRows(db) {
730
+ return db
731
+ .prepare(`SELECT entity_table AS entityTable, uid, birth_fp AS birthFp, reason,
732
+ contested_id AS contestedId, created_at AS createdAt, row_json AS rowJson
733
+ FROM ${QUARANTINE_TABLE} ORDER BY created_at, entity_table, uid`)
734
+ .all().map(({ rowJson, ...rest }) => ({ ...rest, row: JSON.parse(rowJson) }));
735
+ }
736
+ /**
737
+ * The wire form of a local row (what a sender publishes): values without the
738
+ * identity columns, and every declared reference as the target's uid and
739
+ * birth fingerprint.
740
+ *
741
+ * @param db - Connection on the project `cleo.db`.
742
+ * @param table - Declared table.
743
+ * @param uid - The row's uid.
744
+ * @returns The wire row.
745
+ */
746
+ export function wireRowOf(db, table, uid) {
747
+ const spec = specOf(table);
748
+ const row = db.prepare(`SELECT * FROM main.${q(table)} WHERE ${q(UID_COLUMN)} = ?`).get(uid);
749
+ if (!row)
750
+ throw new Error(`wire: no ${table} row with uid ${uid}`);
751
+ const values = {};
752
+ for (const [column, value] of Object.entries(row)) {
753
+ if (column !== UID_COLUMN && column !== BIRTH_FP_COLUMN)
754
+ values[column] = value;
755
+ }
756
+ const refs = {};
757
+ for (const [column, target] of refTargets(spec)) {
758
+ const value = row[column];
759
+ if (value === null || value === undefined) {
760
+ refs[column] = null;
761
+ continue;
762
+ }
763
+ const minted = specOf(target).kind === 'minted';
764
+ const t = db
765
+ .prepare(`SELECT ${q(UID_COLUMN)} AS uid, ${minted ? q(BIRTH_FP_COLUMN) : 'NULL'} AS fp
766
+ FROM main.${q(target)} WHERE ${q(keyOf(target))} = ?`)
767
+ .get(value);
768
+ if (t)
769
+ refs[column] = { uid: t.uid, birthFp: t.fp };
770
+ }
771
+ return {
772
+ table,
773
+ uid,
774
+ birthFp: spec.kind === 'minted' ? (row[BIRTH_FP_COLUMN] ?? null) : null,
775
+ values,
776
+ refs,
777
+ };
778
+ }
779
+ /** Stored-uid copies, display aliases, the re-mint record and the uid alias of a moved row. */
780
+ function followRow(db, table, key, oldUid, birthFp, newUid, options) {
781
+ const moved = [];
782
+ if (key !== null) {
783
+ for (const other of ROW_IDENTITY.project) {
784
+ for (const ref of other.storedRefUids ?? []) {
785
+ if (ref.table !== table || (ref.source ?? 'uid') !== 'uid')
786
+ continue;
787
+ rewriteStoredRefUidNative(db, other.table, ref.column, ref.from, key, oldUid, newUid);
788
+ }
789
+ }
790
+ }
791
+ const aliases = db
792
+ .prepare(`SELECT uid, display_id AS displayId, reason, origin, displaced_hlc AS displacedHlc,
793
+ created_at AS createdAt
794
+ FROM ${DISPLAY_ID_ALIAS_TABLE}
795
+ WHERE entity_table = ? AND entity_uid = ? AND entity_birth_fp = ?`)
796
+ .all(table, oldUid, birthFp);
797
+ for (const a of aliases) {
798
+ deleteDisplayIdAliasNative(db, a.uid);
799
+ const uid = naturalRowUid('project', DISPLAY_ID_ALIAS_TABLE, [table, a.displayId, newUid]);
800
+ insertDisplayIdAliasNative(db, {
801
+ uid,
802
+ entityTable: table,
803
+ displayId: a.displayId,
804
+ entityUid: newUid,
805
+ entityBirthFp: birthFp,
806
+ reason: a.reason,
807
+ origin: a.origin,
808
+ displacedHlc: a.displacedHlc,
809
+ createdAt: a.createdAt,
810
+ });
811
+ moved.push({ table: DISPLAY_ID_ALIAS_TABLE, oldUid: a.uid, newUid: uid });
812
+ }
813
+ if (table === 'tasks_tasks') {
814
+ const remint = readMeta(db, remintKey(oldUid, birthFp));
815
+ if (remint !== undefined) {
816
+ writeRowIdentityMetaNative(db, remintKey(newUid, birthFp), remint);
817
+ deleteRowIdentityMetaNative(db, remintKey(oldUid, birthFp));
818
+ }
819
+ }
820
+ insertUidAliasNative(db, {
821
+ uid: naturalRowUid('project', UID_ALIAS_TABLE, [table, oldUid, birthFp]),
822
+ entityTable: table,
823
+ oldUid,
824
+ oldBirthFp: birthFp,
825
+ newUid,
826
+ origin: options.origin ?? null,
827
+ displacedHlc: options.displacedHlc ?? null,
828
+ createdAt: options.now ?? new Date().toISOString(),
829
+ });
830
+ return moved;
831
+ }
832
+ /** Natural rows keyed by (`table`, `key`): their table, rowid and uid. */
833
+ function naturalRowsKeyedBy(db, table, key) {
834
+ const out = [];
835
+ for (const other of ROW_IDENTITY.project) {
836
+ if (other.kind !== 'natural')
837
+ continue;
838
+ const keyed = (other.keyRefs ?? []).filter((ref) => ref.table === table);
839
+ if (keyed.length === 0)
840
+ continue;
841
+ const rows = db
842
+ .prepare(`SELECT rowid AS rowid, ${q(UID_COLUMN)} AS uid FROM main.${q(other.table)}
843
+ WHERE ${keyed.map((ref) => `${q(ref.column)} = ?`).join(' OR ')}`)
844
+ .all(...keyed.map(() => key));
845
+ for (const r of rows)
846
+ out.push({ table: other.table, ...r });
847
+ }
848
+ return out;
849
+ }
850
+ /** Minted children of a placed row, found through their owner column. */
851
+ function localChildren(db, table, key) {
852
+ const out = [];
853
+ for (const child of ROW_IDENTITY.project) {
854
+ if (child.kind !== 'minted')
855
+ continue;
856
+ for (const owner of child.owners ?? []) {
857
+ if (owner.table !== table)
858
+ continue;
859
+ const rows = db
860
+ .prepare(`SELECT ${q(UID_COLUMN)} AS uid, ${q(BIRTH_FP_COLUMN)} AS birthFp
861
+ FROM main.${q(child.table)}
862
+ WHERE ${q(owner.column)} = ? AND ${q(UID_COLUMN)} IS NOT NULL
863
+ AND ${q(BIRTH_FP_COLUMN)} IS NOT NULL
864
+ ORDER BY ${q(UID_COLUMN)}`)
865
+ .all(key);
866
+ for (const r of rows)
867
+ out.push({ table: child.table, ...r });
868
+ }
869
+ }
870
+ return out;
871
+ }
872
+ /** Held minted children of a held row, found through their wire owner ref. */
873
+ function heldChildren(db, table, uid, birthFp) {
874
+ const out = [];
875
+ for (const h of listHeldRows(db)) {
876
+ const spec = rowIdentitySpec('project', h.entityTable);
877
+ if (spec?.kind !== 'minted')
878
+ continue;
879
+ for (const owner of spec.owners ?? []) {
880
+ if (owner.table !== table)
881
+ continue;
882
+ const ref = h.row.refs?.[owner.column];
883
+ if (ref && ref.uid === uid && ref.birthFp === birthFp) {
884
+ out.push({ table: h.entityTable, uid: h.uid, birthFp: h.birthFp });
885
+ }
886
+ }
887
+ }
888
+ return out;
889
+ }
890
+ /** Re-key a held row's wire form. */
891
+ function rekeyHeldRow(db, key, newUid) {
892
+ const row = db
893
+ .prepare(`SELECT row_json AS rowJson FROM ${QUARANTINE_TABLE}
894
+ WHERE entity_table = ? AND uid = ? AND birth_fp = ?`)
895
+ .get(key.entityTable, key.uid, key.birthFp);
896
+ const wire = JSON.parse(row.rowJson);
897
+ rekeyQuarantineNative(db, key, newUid, JSON.stringify({ ...wire, uid: newUid }));
898
+ }
899
+ /** Where a (table, uid, birth fp) row is on this replica. */
900
+ function locate(db, table, uid, birthFp) {
901
+ const local = localRow(db, table, uid, birthFp);
902
+ if (local)
903
+ return { where: 'local', key: local.key };
904
+ const held = db
905
+ .prepare(`SELECT 1 AS x FROM ${QUARANTINE_TABLE} WHERE entity_table = ? AND uid = ? AND birth_fp = ?`)
906
+ .get(table, uid, birthFp);
907
+ return held ? { where: 'held' } : { where: 'absent' };
908
+ }
909
+ /** AUTHORITY: re-key one row wherever it is, derive its cascade, and return the receipt. */
910
+ function rekeyDerive(db, table, oldUid, birthFp, newUid, options) {
911
+ const at = locate(db, table, oldUid, birthFp);
912
+ const natural = [];
913
+ let children;
914
+ if (at.where === 'local') {
915
+ children = localChildren(db, table, at.key);
916
+ const keyed = naturalRowsKeyedBy(db, table, at.key);
917
+ setRowUidNative(db, table, oldUid, birthFp, newUid);
918
+ natural.push(...followRow(db, table, at.key, oldUid, birthFp, newUid, options));
919
+ for (const n of keyed)
920
+ clearRowUidNative(db, n.table, n.rowid);
921
+ for (const t of new Set(keyed.map((n) => n.table)))
922
+ fillTableUids(db, 'project', t);
923
+ for (const n of keyed) {
924
+ const now = db
925
+ .prepare(`SELECT ${q(UID_COLUMN)} AS uid FROM main.${q(n.table)} WHERE rowid = ?`)
926
+ .get(n.rowid);
927
+ if (n.uid && now.uid && n.uid !== now.uid) {
928
+ natural.push({ table: n.table, oldUid: n.uid, newUid: now.uid });
929
+ }
930
+ }
931
+ }
932
+ else if (at.where === 'held') {
933
+ children = heldChildren(db, table, oldUid, birthFp);
934
+ rekeyHeldRow(db, { entityTable: table, uid: oldUid, birthFp }, newUid);
935
+ followRow(db, table, null, oldUid, birthFp, newUid, options);
936
+ }
937
+ else {
938
+ throw new CleoError(ExitCode.NOT_FOUND, `No ${table} row ${oldUid} with birth ${birthFp}`, {
939
+ details: { field: 'uid', actual: oldUid },
940
+ });
941
+ }
942
+ const cascaded = children.map((c) => rekeyDerive(db, c.table, c.uid, c.birthFp, rekeyedChildUid('project', c.table, c.uid, newUid), options));
943
+ return {
944
+ table,
945
+ oldUid,
946
+ birthFp,
947
+ newUid,
948
+ natural,
949
+ cascaded,
950
+ origin: options.origin ?? null,
951
+ hlc: options.displacedHlc ?? null,
952
+ };
953
+ }
954
+ /**
955
+ * AUTHORITY ONLY: re-key the LOSER of a uid collision (same uid, different
956
+ * birth fingerprints; the loser is the greater fingerprint). The caller names
957
+ * the loser by its fingerprint and the call asserts it: the row re-keyed is
958
+ * the one with `loserBirthFp`, wherever it is here (placed, or held because
959
+ * the winner arrived first), and a fingerprint that is not the greater of the
960
+ * pair is refused. The row gets a random uid; the cascade follows from stored
961
+ * identity (module docs). Held rows the re-key unblocks are placed.
962
+ *
963
+ * @param db - Connection on the project `cleo.db`.
964
+ * @param table - Minted table of the row.
965
+ * @param uid - The colliding uid.
966
+ * @param loserBirthFp - The loser's birth fingerprint.
967
+ * @param options - The authority and the HLC of the re-key.
968
+ * @returns The receipt to publish, with its cascade and natural uids.
969
+ * @throws CleoError when this replica has no row (uid, loserBirthFp), or the
970
+ * fingerprint is not the loser's.
971
+ */
972
+ export function rekeyRowUid(db, table, uid, loserBirthFp, options = {}) {
973
+ if (specOf(table).kind !== 'minted') {
974
+ throw new Error(`rekey: ${table} is not a declared minted table`);
975
+ }
976
+ const rivals = [
977
+ ...db
978
+ .prepare(`SELECT ${q(BIRTH_FP_COLUMN)} AS fp FROM main.${q(table)} WHERE ${q(UID_COLUMN)} = ?`)
979
+ .all(uid).map((r) => r.fp),
980
+ ...db
981
+ .prepare(`SELECT birth_fp AS fp FROM ${QUARANTINE_TABLE} WHERE entity_table = ? AND uid = ?`)
982
+ .all(table, uid).map((r) => r.fp),
983
+ ].filter((fp) => fp !== null && fp !== loserBirthFp);
984
+ const greater = rivals.find((fp) => fp > loserBirthFp);
985
+ if (greater !== undefined) {
986
+ throw new CleoError(ExitCode.VALIDATION_ERROR, `rekey: ${loserBirthFp} is not the loser of the ${table} uid collision on ${uid}`, {
987
+ fix: 'Re-key the row with the greater birth fingerprint; the smaller one keeps the uid.',
988
+ details: { field: 'loserBirthFp', actual: loserBirthFp, expected: greater },
989
+ });
990
+ }
991
+ if (locate(db, table, uid, loserBirthFp).where === 'absent') {
992
+ throw new CleoError(ExitCode.NOT_FOUND, `No ${table} row with uid ${uid} and birth fingerprint ${loserBirthFp} on this replica`, {
993
+ fix: 'Only a replica that holds the loser re-keys it; the others apply the receipt.',
994
+ details: { field: 'uid', actual: uid },
995
+ });
996
+ }
997
+ registerRowUidFunction(db, 'project');
998
+ return inSavepoint(db, 'rekey', () => {
999
+ const receipt = rekeyDerive(db, table, uid, loserBirthFp, mintRowUid(), options);
1000
+ releaseHeldRows(db);
1001
+ return receipt;
1002
+ });
1003
+ }
1004
+ /** Apply one receipt entry, then its cascade. */
1005
+ function applyEntry(db, e, out) {
1006
+ const options = { origin: e.origin, displacedHlc: e.hlc };
1007
+ const at = locate(db, e.table, e.oldUid, e.birthFp);
1008
+ let status;
1009
+ if (at.where === 'local') {
1010
+ const keyed = naturalRowsKeyedBy(db, e.table, at.key);
1011
+ setRowUidNative(db, e.table, e.oldUid, e.birthFp, e.newUid);
1012
+ followRow(db, e.table, at.key, e.oldUid, e.birthFp, e.newUid, options);
1013
+ for (const n of e.natural) {
1014
+ if (n.table !== DISPLAY_ID_ALIAS_TABLE)
1015
+ setNaturalUidNative(db, n.table, n.oldUid, n.newUid);
1016
+ }
1017
+ // A natural row the authority did not have (an edge only this replica
1018
+ // holds) is re-derived here; for the published ones this is a no-op.
1019
+ for (const n of keyed)
1020
+ clearRowUidNative(db, n.table, n.rowid);
1021
+ for (const t of new Set(keyed.map((n) => n.table)))
1022
+ fillTableUids(db, 'project', t);
1023
+ status = 'applied';
1024
+ }
1025
+ else if (at.where === 'held') {
1026
+ rekeyHeldRow(db, { entityTable: e.table, uid: e.oldUid, birthFp: e.birthFp }, e.newUid);
1027
+ followRow(db, e.table, null, e.oldUid, e.birthFp, e.newUid, options);
1028
+ status = 'applied-held';
1029
+ }
1030
+ else {
1031
+ status =
1032
+ locate(db, e.table, e.newUid, e.birthFp).where === 'absent'
1033
+ ? 'alias-only'
1034
+ : 'already-applied';
1035
+ followRow(db, e.table, null, e.oldUid, e.birthFp, e.newUid, options);
1036
+ }
1037
+ out.push({ table: e.table, oldUid: e.oldUid, status });
1038
+ for (const child of e.cascaded)
1039
+ applyEntry(db, child, out);
1040
+ }
1041
+ /**
1042
+ * RECEIVER: apply a published {@link RekeyReceipt}. Every entry moves the row
1043
+ * with (old uid, birth fingerprint) to the published new uid, wherever it is
1044
+ * here: a placed row (with its stored copies, display aliases and the
1045
+ * published natural uids), a held row (its wire form), or nowhere (the uid
1046
+ * alias is recorded, so the row takes its new uid when it arrives). A row
1047
+ * with the old uid and ANOTHER fingerprint (the winner) is never touched.
1048
+ * Nothing is recomputed. Held rows the re-key unblocks are placed.
1049
+ *
1050
+ * @param db - Connection on the project `cleo.db`.
1051
+ * @param receipt - The published receipt.
1052
+ * @returns Per-row outcome, and the held rows placed.
1053
+ */
1054
+ export function applyRekey(db, receipt) {
1055
+ registerRowUidFunction(db, 'project');
1056
+ return inSavepoint(db, 'rekey_apply', () => {
1057
+ markRowIdentityShared(db, 'receive');
1058
+ const rows = [];
1059
+ applyEntry(db, receipt, rows);
1060
+ return { rows, released: releaseHeldRows(db) };
1061
+ });
1062
+ }
1063
+ //# sourceMappingURL=display-id-alias.js.map