@cleocode/core 2026.9.24 → 2026.9.26

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 (481) hide show
  1. package/dist/admin/import-tasks.d.ts.map +1 -1
  2. package/dist/admin/import-tasks.js +5 -2
  3. package/dist/admin/import-tasks.js.map +1 -1
  4. package/dist/admin/import.d.ts.map +1 -1
  5. package/dist/admin/import.js +7 -2
  6. package/dist/admin/import.js.map +1 -1
  7. package/dist/audit-prune.d.ts +32 -0
  8. package/dist/audit-prune.d.ts.map +1 -1
  9. package/dist/audit-prune.js +37 -3
  10. package/dist/audit-prune.js.map +1 -1
  11. package/dist/build-provenance.json +2 -2
  12. package/dist/cloud/crypto.d.ts +1 -2
  13. package/dist/cloud/crypto.d.ts.map +1 -1
  14. package/dist/cloud/crypto.js +1 -11
  15. package/dist/cloud/crypto.js.map +1 -1
  16. package/dist/cloud/index.d.ts +1 -0
  17. package/dist/cloud/index.d.ts.map +1 -1
  18. package/dist/cloud/index.js +1 -0
  19. package/dist/cloud/index.js.map +1 -1
  20. package/dist/cloud/nexus-auth.d.ts +26 -1
  21. package/dist/cloud/nexus-auth.d.ts.map +1 -1
  22. package/dist/cloud/nexus-auth.js +61 -32
  23. package/dist/cloud/nexus-auth.js.map +1 -1
  24. package/dist/cloud/nexus-credentials.js +2 -2
  25. package/dist/cloud/nexus-credentials.js.map +1 -1
  26. package/dist/cloud/nexus-device.d.ts +809 -0
  27. package/dist/cloud/nexus-device.d.ts.map +1 -0
  28. package/dist/cloud/nexus-device.js +1680 -0
  29. package/dist/cloud/nexus-device.js.map +1 -0
  30. package/dist/cloud/nexus-enrol.d.ts +210 -0
  31. package/dist/cloud/nexus-enrol.d.ts.map +1 -0
  32. package/dist/cloud/nexus-enrol.js +1023 -0
  33. package/dist/cloud/nexus-enrol.js.map +1 -0
  34. package/dist/cloud/nexus-link.d.ts +3 -0
  35. package/dist/cloud/nexus-link.d.ts.map +1 -1
  36. package/dist/cloud/nexus-link.js +38 -4
  37. package/dist/cloud/nexus-link.js.map +1 -1
  38. package/dist/cloud/signing.d.ts +20 -0
  39. package/dist/cloud/signing.d.ts.map +1 -1
  40. package/dist/cloud/signing.js +14 -0
  41. package/dist/cloud/signing.js.map +1 -1
  42. package/dist/cloud/uuidv7.d.ts +9 -0
  43. package/dist/cloud/uuidv7.d.ts.map +1 -0
  44. package/dist/cloud/uuidv7.js +19 -0
  45. package/dist/cloud/uuidv7.js.map +1 -0
  46. package/dist/crypto/credentials.d.ts +61 -0
  47. package/dist/crypto/credentials.d.ts.map +1 -1
  48. package/dist/crypto/credentials.js +154 -58
  49. package/dist/crypto/credentials.js.map +1 -1
  50. package/dist/decide/bench/index.d.ts +1 -1
  51. package/dist/decide/bench/index.d.ts.map +1 -1
  52. package/dist/decide/bench/index.js +1 -1
  53. package/dist/decide/bench/index.js.map +1 -1
  54. package/dist/decide/bench/operation.d.ts +1 -1
  55. package/dist/decide/bench/operation.d.ts.map +1 -1
  56. package/dist/decide/bench/operation.js +1 -1
  57. package/dist/decide/bench/operation.js.map +1 -1
  58. package/dist/decide/bench/profiles.d.ts +50 -22
  59. package/dist/decide/bench/profiles.d.ts.map +1 -1
  60. package/dist/decide/bench/profiles.js +103 -59
  61. package/dist/decide/bench/profiles.js.map +1 -1
  62. package/dist/decide/bench/report.d.ts.map +1 -1
  63. package/dist/decide/bench/report.js +6 -1
  64. package/dist/decide/bench/report.js.map +1 -1
  65. package/dist/decide/bench/runner.d.ts +6 -1
  66. package/dist/decide/bench/runner.d.ts.map +1 -1
  67. package/dist/decide/bench/runner.js +1 -0
  68. package/dist/decide/bench/runner.js.map +1 -1
  69. package/dist/decide/client.d.ts.map +1 -1
  70. package/dist/decide/client.js +11 -5
  71. package/dist/decide/client.js.map +1 -1
  72. package/dist/decide/jev-wire.d.ts +31 -1
  73. package/dist/decide/jev-wire.d.ts.map +1 -1
  74. package/dist/decide/jev-wire.js +61 -7
  75. package/dist/decide/jev-wire.js.map +1 -1
  76. package/dist/docs/export-document.js +12907 -10622
  77. package/dist/docs/export-document.js.map +3 -3
  78. package/dist/doctor/dependency-cycles.d.ts +41 -0
  79. package/dist/doctor/dependency-cycles.d.ts.map +1 -0
  80. package/dist/doctor/dependency-cycles.js +39 -0
  81. package/dist/doctor/dependency-cycles.js.map +1 -0
  82. package/dist/doctor/index.d.ts +2 -0
  83. package/dist/doctor/index.d.ts.map +1 -1
  84. package/dist/doctor/index.js +1 -0
  85. package/dist/doctor/index.js.map +1 -1
  86. package/dist/doctor/orphan-ac-bindings.d.ts +64 -0
  87. package/dist/doctor/orphan-ac-bindings.d.ts.map +1 -0
  88. package/dist/doctor/orphan-ac-bindings.js +59 -0
  89. package/dist/doctor/orphan-ac-bindings.js.map +1 -0
  90. package/dist/doctor/projects.d.ts.map +1 -1
  91. package/dist/doctor/projects.js +43 -29
  92. package/dist/doctor/projects.js.map +1 -1
  93. package/dist/doctor/row-identity.d.ts +42 -0
  94. package/dist/doctor/row-identity.d.ts.map +1 -0
  95. package/dist/doctor/row-identity.js +151 -0
  96. package/dist/doctor/row-identity.js.map +1 -0
  97. package/dist/doctor/twin-collapse.d.ts +58 -2
  98. package/dist/doctor/twin-collapse.d.ts.map +1 -1
  99. package/dist/doctor/twin-collapse.js +212 -14
  100. package/dist/doctor/twin-collapse.js.map +1 -1
  101. package/dist/error-catalog.d.ts.map +1 -1
  102. package/dist/error-catalog.js +4 -0
  103. package/dist/error-catalog.js.map +1 -1
  104. package/dist/error-registry.d.ts.map +1 -1
  105. package/dist/error-registry.js +8 -0
  106. package/dist/error-registry.js.map +1 -1
  107. package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
  108. package/dist/lifecycle/verification/satisfies-validator.js +10 -0
  109. package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
  110. package/dist/llm/plugin-facade.js +17767 -15482
  111. package/dist/llm/plugin-facade.js.map +3 -3
  112. package/dist/memory/index.d.ts.map +1 -1
  113. package/dist/memory/index.js +19 -10
  114. package/dist/memory/index.js.map +1 -1
  115. package/dist/memory/manifest-identity.d.ts +171 -0
  116. package/dist/memory/manifest-identity.d.ts.map +1 -0
  117. package/dist/memory/manifest-identity.js +345 -0
  118. package/dist/memory/manifest-identity.js.map +1 -0
  119. package/dist/memory/mental-model-queue.d.ts +14 -4
  120. package/dist/memory/mental-model-queue.d.ts.map +1 -1
  121. package/dist/memory/mental-model-queue.js +64 -35
  122. package/dist/memory/mental-model-queue.js.map +1 -1
  123. package/dist/memory/pipeline-manifest-sqlite.d.ts +47 -5
  124. package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
  125. package/dist/memory/pipeline-manifest-sqlite.js +130 -14
  126. package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
  127. package/dist/memory/retrieval/observe.d.ts.map +1 -1
  128. package/dist/memory/retrieval/observe.js +2 -0
  129. package/dist/memory/retrieval/observe.js.map +1 -1
  130. package/dist/memory/transcript-scanner.js +1 -1
  131. package/dist/nexus/deps.d.ts.map +1 -1
  132. package/dist/nexus/deps.js +7 -14
  133. package/dist/nexus/deps.js.map +1 -1
  134. package/dist/nexus/discover.d.ts.map +1 -1
  135. package/dist/nexus/discover.js +6 -9
  136. package/dist/nexus/discover.js.map +1 -1
  137. package/dist/nexus/git-state.d.ts.map +1 -1
  138. package/dist/nexus/git-state.js +10 -1
  139. package/dist/nexus/git-state.js.map +1 -1
  140. package/dist/nexus/projects-clean.d.ts.map +1 -1
  141. package/dist/nexus/projects-clean.js +22 -11
  142. package/dist/nexus/projects-clean.js.map +1 -1
  143. package/dist/nexus/projects-scan.d.ts.map +1 -1
  144. package/dist/nexus/projects-scan.js +8 -13
  145. package/dist/nexus/projects-scan.js.map +1 -1
  146. package/dist/nexus/query.d.ts.map +1 -1
  147. package/dist/nexus/query.js +4 -12
  148. package/dist/nexus/query.js.map +1 -1
  149. package/dist/nexus/registry-roots.d.ts +5 -2
  150. package/dist/nexus/registry-roots.d.ts.map +1 -1
  151. package/dist/nexus/registry-roots.js +8 -10
  152. package/dist/nexus/registry-roots.js.map +1 -1
  153. package/dist/nexus/registry.d.ts +18 -0
  154. package/dist/nexus/registry.d.ts.map +1 -1
  155. package/dist/nexus/registry.js +37 -16
  156. package/dist/nexus/registry.js.map +1 -1
  157. package/dist/nexus/workspace.d.ts.map +1 -1
  158. package/dist/nexus/workspace.js +54 -4
  159. package/dist/nexus/workspace.js.map +1 -1
  160. package/dist/orchestrate/lifecycle-ops.js +1 -1
  161. package/dist/orchestrate/lifecycle-ops.js.map +1 -1
  162. package/dist/orchestrate/pivot.d.ts.map +1 -1
  163. package/dist/orchestrate/pivot.js +4 -2
  164. package/dist/orchestrate/pivot.js.map +1 -1
  165. package/dist/orchestrate/query-ops.d.ts.map +1 -1
  166. package/dist/orchestrate/query-ops.js +3 -3
  167. package/dist/orchestrate/query-ops.js.map +1 -1
  168. package/dist/orchestrate/spawn-ops.js +1 -1
  169. package/dist/orchestrate/spawn-ops.js.map +1 -1
  170. package/dist/orchestrate/worktree-complete.d.ts.map +1 -1
  171. package/dist/orchestrate/worktree-complete.js +40 -2
  172. package/dist/orchestrate/worktree-complete.js.map +1 -1
  173. package/dist/orchestration/index.js +1 -1
  174. package/dist/orchestration/index.js.map +1 -1
  175. package/dist/orchestration/lead-rollup.d.ts.map +1 -1
  176. package/dist/orchestration/lead-rollup.js +3 -3
  177. package/dist/orchestration/lead-rollup.js.map +1 -1
  178. package/dist/orchestration/parallel.js +1 -1
  179. package/dist/orchestration/parallel.js.map +1 -1
  180. package/dist/orchestration/spawn-prompt.d.ts +26 -0
  181. package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
  182. package/dist/orchestration/spawn-prompt.js +62 -91
  183. package/dist/orchestration/spawn-prompt.js.map +1 -1
  184. package/dist/orchestration/wave-topic.d.ts +4 -2
  185. package/dist/orchestration/wave-topic.d.ts.map +1 -1
  186. package/dist/orchestration/wave-topic.js +6 -4
  187. package/dist/orchestration/wave-topic.js.map +1 -1
  188. package/dist/orchestration/waves.d.ts +2 -1
  189. package/dist/orchestration/waves.d.ts.map +1 -1
  190. package/dist/orchestration/waves.js +4 -3
  191. package/dist/orchestration/waves.js.map +1 -1
  192. package/dist/phases/index.d.ts.map +1 -1
  193. package/dist/phases/index.js +4 -2
  194. package/dist/phases/index.js.map +1 -1
  195. package/dist/release/ci-evidence.d.ts +190 -0
  196. package/dist/release/ci-evidence.d.ts.map +1 -1
  197. package/dist/release/ci-evidence.js +436 -4
  198. package/dist/release/ci-evidence.js.map +1 -1
  199. package/dist/sdk/validator-ac-pull.d.ts.map +1 -1
  200. package/dist/sdk/validator-ac-pull.js +2 -1
  201. package/dist/sdk/validator-ac-pull.js.map +1 -1
  202. package/dist/selfimprove/fix-gen.js +12807 -10522
  203. package/dist/selfimprove/fix-gen.js.map +3 -3
  204. package/dist/sentient/cross-project-hygiene.d.ts +5 -0
  205. package/dist/sentient/cross-project-hygiene.d.ts.map +1 -1
  206. package/dist/sentient/cross-project-hygiene.js +7 -1
  207. package/dist/sentient/cross-project-hygiene.js.map +1 -1
  208. package/dist/sentient/propose-tick.d.ts.map +1 -1
  209. package/dist/sentient/propose-tick.js +2 -3
  210. package/dist/sentient/propose-tick.js.map +1 -1
  211. package/dist/sentient/stage-drift-tick.d.ts.map +1 -1
  212. package/dist/sentient/stage-drift-tick.js +2 -3
  213. package/dist/sentient/stage-drift-tick.js.map +1 -1
  214. package/dist/session/engine-ops.d.ts.map +1 -1
  215. package/dist/session/engine-ops.js +77 -49
  216. package/dist/session/engine-ops.js.map +1 -1
  217. package/dist/sessions/focus-state-store.d.ts +69 -23
  218. package/dist/sessions/focus-state-store.d.ts.map +1 -1
  219. package/dist/sessions/focus-state-store.js +113 -27
  220. package/dist/sessions/focus-state-store.js.map +1 -1
  221. package/dist/sessions/handoff.js +1 -1
  222. package/dist/sessions/handoff.js.map +1 -1
  223. package/dist/sessions/index.d.ts.map +1 -1
  224. package/dist/sessions/index.js +24 -13
  225. package/dist/sessions/index.js.map +1 -1
  226. package/dist/sessions/terminal-identity.d.ts +165 -17
  227. package/dist/sessions/terminal-identity.d.ts.map +1 -1
  228. package/dist/sessions/terminal-identity.js +341 -32
  229. package/dist/sessions/terminal-identity.js.map +1 -1
  230. package/dist/skills/manifests/research.js +2 -0
  231. package/dist/skills/manifests/research.js.map +1 -1
  232. package/dist/skills/orchestrator/startup.js +2 -2
  233. package/dist/skills/orchestrator/startup.js.map +1 -1
  234. package/dist/skills/orchestrator/validator.d.ts.map +1 -1
  235. package/dist/skills/orchestrator/validator.js +9 -2
  236. package/dist/skills/orchestrator/validator.js.map +1 -1
  237. package/dist/skills/validation.d.ts +4 -1
  238. package/dist/skills/validation.d.ts.map +1 -1
  239. package/dist/skills/validation.js +11 -3
  240. package/dist/skills/validation.js.map +1 -1
  241. package/dist/snapshot/index.d.ts +4 -0
  242. package/dist/snapshot/index.d.ts.map +1 -1
  243. package/dist/snapshot/index.js +50 -3
  244. package/dist/snapshot/index.js.map +1 -1
  245. package/dist/spawn/branch-lock.d.ts +111 -7
  246. package/dist/spawn/branch-lock.d.ts.map +1 -1
  247. package/dist/spawn/branch-lock.js +296 -8
  248. package/dist/spawn/branch-lock.js.map +1 -1
  249. package/dist/store/ac-binding-prune.d.ts +144 -0
  250. package/dist/store/ac-binding-prune.d.ts.map +1 -0
  251. package/dist/store/ac-binding-prune.js +293 -0
  252. package/dist/store/ac-binding-prune.js.map +1 -0
  253. package/dist/store/agent-registry-store.d.ts.map +1 -1
  254. package/dist/store/agent-registry-store.js +3 -2
  255. package/dist/store/agent-registry-store.js.map +1 -1
  256. package/dist/store/backup-sidecar.d.ts +85 -2
  257. package/dist/store/backup-sidecar.d.ts.map +1 -1
  258. package/dist/store/backup-sidecar.js +173 -1
  259. package/dist/store/backup-sidecar.js.map +1 -1
  260. package/dist/store/conduit-sqlite.d.ts.map +1 -1
  261. package/dist/store/conduit-sqlite.js +3 -2
  262. package/dist/store/conduit-sqlite.js.map +1 -1
  263. package/dist/store/credential-transfer.d.ts.map +1 -1
  264. package/dist/store/credential-transfer.js +3 -0
  265. package/dist/store/credential-transfer.js.map +1 -1
  266. package/dist/store/db-helpers.d.ts +32 -13
  267. package/dist/store/db-helpers.d.ts.map +1 -1
  268. package/dist/store/db-helpers.js +144 -40
  269. package/dist/store/db-helpers.js.map +1 -1
  270. package/dist/store/dependency-cycles.d.ts +164 -0
  271. package/dist/store/dependency-cycles.d.ts.map +1 -0
  272. package/dist/store/dependency-cycles.js +360 -0
  273. package/dist/store/dependency-cycles.js.map +1 -0
  274. package/dist/store/display-id-alias.d.ts +585 -0
  275. package/dist/store/display-id-alias.d.ts.map +1 -0
  276. package/dist/store/display-id-alias.js +1538 -0
  277. package/dist/store/display-id-alias.js.map +1 -0
  278. package/dist/store/dual-scope-db.d.ts.map +1 -1
  279. package/dist/store/dual-scope-db.js +48 -0
  280. package/dist/store/dual-scope-db.js.map +1 -1
  281. package/dist/store/exodus/migrate.d.ts.map +1 -1
  282. package/dist/store/exodus/migrate.js +8 -1
  283. package/dist/store/exodus/migrate.js.map +1 -1
  284. package/dist/store/exodus/reconcile.d.ts.map +1 -1
  285. package/dist/store/exodus/reconcile.js +7 -2
  286. package/dist/store/exodus/reconcile.js.map +1 -1
  287. package/dist/store/exodus/recovery.d.ts.map +1 -1
  288. package/dist/store/exodus/recovery.js +20 -1
  289. package/dist/store/exodus/recovery.js.map +1 -1
  290. package/dist/store/global-salt.d.ts +35 -0
  291. package/dist/store/global-salt.d.ts.map +1 -1
  292. package/dist/store/global-salt.js +136 -16
  293. package/dist/store/global-salt.js.map +1 -1
  294. package/dist/store/legacy-tasks-lineage.d.ts.map +1 -1
  295. package/dist/store/legacy-tasks-lineage.js +19 -2
  296. package/dist/store/legacy-tasks-lineage.js.map +1 -1
  297. package/dist/store/migration-sqlite.d.ts.map +1 -1
  298. package/dist/store/migration-sqlite.js +19 -11
  299. package/dist/store/migration-sqlite.js.map +1 -1
  300. package/dist/store/portable-bundle-scan.d.ts.map +1 -1
  301. package/dist/store/portable-bundle-scan.js +6 -0
  302. package/dist/store/portable-bundle-scan.js.map +1 -1
  303. package/dist/store/row-identity-flag.d.ts +26 -0
  304. package/dist/store/row-identity-flag.d.ts.map +1 -0
  305. package/dist/store/row-identity-flag.js +31 -0
  306. package/dist/store/row-identity-flag.js.map +1 -0
  307. package/dist/store/row-identity-registry.d.ts +61 -0
  308. package/dist/store/row-identity-registry.d.ts.map +1 -0
  309. package/dist/store/row-identity-registry.js +206 -0
  310. package/dist/store/row-identity-registry.js.map +1 -0
  311. package/dist/store/row-identity.d.ts +480 -0
  312. package/dist/store/row-identity.d.ts.map +1 -0
  313. package/dist/store/row-identity.js +1509 -0
  314. package/dist/store/row-identity.js.map +1 -0
  315. package/dist/store/safety-data-accessor.d.ts +5 -2
  316. package/dist/store/safety-data-accessor.d.ts.map +1 -1
  317. package/dist/store/safety-data-accessor.js +9 -2
  318. package/dist/store/safety-data-accessor.js.map +1 -1
  319. package/dist/store/schema/attachments.d.ts +1 -1
  320. package/dist/store/schema/audit.d.ts +4 -4
  321. package/dist/store/schema/background-jobs.d.ts +1 -1
  322. package/dist/store/schema/cleo-global/nexus.d.ts +1 -1
  323. package/dist/store/schema/cleo-global/skills.d.ts +2 -2
  324. package/dist/store/schema/cleo-project/audit.d.ts +4 -4
  325. package/dist/store/schema/cleo-project/docs.d.ts +1 -1
  326. package/dist/store/schema/cleo-project/lifecycle.d.ts +4 -4
  327. package/dist/store/schema/cleo-project/nexus-graph.d.ts +3 -3
  328. package/dist/store/schema/cleo-project/provenance-commits.d.ts +2 -2
  329. package/dist/store/schema/cleo-project/provenance-rest.d.ts +3 -3
  330. package/dist/store/schema/cleo-project/runtime.d.ts +3 -3
  331. package/dist/store/schema/cleo-project/tasks-core-batch2.d.ts +87 -1
  332. package/dist/store/schema/cleo-project/tasks-core-batch2.d.ts.map +1 -1
  333. package/dist/store/schema/cleo-project/tasks-core-batch2.js +35 -0
  334. package/dist/store/schema/cleo-project/tasks-core-batch2.js.map +1 -1
  335. package/dist/store/schema/cleo-project/tasks-core.d.ts +183 -7
  336. package/dist/store/schema/cleo-project/tasks-core.d.ts.map +1 -1
  337. package/dist/store/schema/cleo-project/tasks-core.js +52 -1
  338. package/dist/store/schema/cleo-project/tasks-core.js.map +1 -1
  339. package/dist/store/schema/cleo-shared/brain.d.ts +6 -6
  340. package/dist/store/schema/lifecycle.d.ts +4 -4
  341. package/dist/store/schema/manifest.d.ts +1 -1
  342. package/dist/store/schema/memory-schema.d.ts +5 -5
  343. package/dist/store/schema/nexus-schema.d.ts +3 -3
  344. package/dist/store/schema/provenance/releases.d.ts +1 -1
  345. package/dist/store/schema/skills-schema.d.ts +2 -2
  346. package/dist/store/schema/tasks.d.ts +6 -6
  347. package/dist/store/session-store.d.ts +63 -3
  348. package/dist/store/session-store.d.ts.map +1 -1
  349. package/dist/store/session-store.js +121 -8
  350. package/dist/store/session-store.js.map +1 -1
  351. package/dist/store/split-brain-import.d.ts +2 -14
  352. package/dist/store/split-brain-import.d.ts.map +1 -1
  353. package/dist/store/split-brain-import.js +40 -19
  354. package/dist/store/split-brain-import.js.map +1 -1
  355. package/dist/store/sqlite-data-accessor.d.ts +326 -2
  356. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  357. package/dist/store/sqlite-data-accessor.js +782 -49
  358. package/dist/store/sqlite-data-accessor.js.map +1 -1
  359. package/dist/store/sync/clock-store.d.ts +67 -0
  360. package/dist/store/sync/clock-store.d.ts.map +1 -0
  361. package/dist/store/sync/clock-store.js +117 -0
  362. package/dist/store/sync/clock-store.js.map +1 -0
  363. package/dist/store/sync/flags.d.ts +51 -0
  364. package/dist/store/sync/flags.d.ts.map +1 -0
  365. package/dist/store/sync/flags.js +81 -0
  366. package/dist/store/sync/flags.js.map +1 -0
  367. package/dist/store/sync/hlc.d.ts +115 -0
  368. package/dist/store/sync/hlc.d.ts.map +1 -0
  369. package/dist/store/sync/hlc.js +172 -0
  370. package/dist/store/sync/hlc.js.map +1 -0
  371. package/dist/store/sync/index.d.ts +20 -0
  372. package/dist/store/sync/index.d.ts.map +1 -0
  373. package/dist/store/sync/index.js +20 -0
  374. package/dist/store/sync/index.js.map +1 -0
  375. package/dist/store/sync/replica-registry.d.ts +85 -0
  376. package/dist/store/sync/replica-registry.d.ts.map +1 -0
  377. package/dist/store/sync/replica-registry.js +147 -0
  378. package/dist/store/sync/replica-registry.js.map +1 -0
  379. package/dist/store/sync/replica.d.ts +181 -0
  380. package/dist/store/sync/replica.d.ts.map +1 -0
  381. package/dist/store/sync/replica.js +305 -0
  382. package/dist/store/sync/replica.js.map +1 -0
  383. package/dist/store/sync/schema.d.ts +68 -0
  384. package/dist/store/sync/schema.d.ts.map +1 -0
  385. package/dist/store/sync/schema.js +130 -0
  386. package/dist/store/sync/schema.js.map +1 -0
  387. package/dist/store/sync/skew-hold.d.ts +71 -0
  388. package/dist/store/sync/skew-hold.d.ts.map +1 -0
  389. package/dist/store/sync/skew-hold.js +89 -0
  390. package/dist/store/sync/skew-hold.js.map +1 -0
  391. package/dist/store/table-classification.d.ts +10 -8
  392. package/dist/store/table-classification.d.ts.map +1 -1
  393. package/dist/store/table-classification.js +93 -25
  394. package/dist/store/table-classification.js.map +1 -1
  395. package/dist/store/tasks-sqlite.d.ts.map +1 -1
  396. package/dist/store/tasks-sqlite.js +23 -3
  397. package/dist/store/tasks-sqlite.js.map +1 -1
  398. package/dist/store/twin-collapse.d.ts +113 -2
  399. package/dist/store/twin-collapse.d.ts.map +1 -1
  400. package/dist/store/twin-collapse.js +247 -19
  401. package/dist/store/twin-collapse.js.map +1 -1
  402. package/dist/store/umbrella-data-accessor.d.ts +5 -2
  403. package/dist/store/umbrella-data-accessor.d.ts.map +1 -1
  404. package/dist/store/umbrella-data-accessor.js +9 -2
  405. package/dist/store/umbrella-data-accessor.js.map +1 -1
  406. package/dist/store/validation-schemas.d.ts +241 -49
  407. package/dist/store/validation-schemas.d.ts.map +1 -1
  408. package/dist/system/health.d.ts +2 -0
  409. package/dist/system/health.d.ts.map +1 -1
  410. package/dist/system/health.js +57 -0
  411. package/dist/system/health.js.map +1 -1
  412. package/dist/system/project-health.d.ts +9 -2
  413. package/dist/system/project-health.d.ts.map +1 -1
  414. package/dist/system/project-health.js +8 -3
  415. package/dist/system/project-health.js.map +1 -1
  416. package/dist/task-work/index.d.ts.map +1 -1
  417. package/dist/task-work/index.js +9 -6
  418. package/dist/task-work/index.js.map +1 -1
  419. package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
  420. package/dist/tasks/ac-coverage-gate.js +3 -0
  421. package/dist/tasks/ac-coverage-gate.js.map +1 -1
  422. package/dist/tasks/ac-table.d.ts +35 -7
  423. package/dist/tasks/ac-table.d.ts.map +1 -1
  424. package/dist/tasks/ac-table.js +119 -14
  425. package/dist/tasks/ac-table.js.map +1 -1
  426. package/dist/tasks/add.d.ts.map +1 -1
  427. package/dist/tasks/add.js +48 -2
  428. package/dist/tasks/add.js.map +1 -1
  429. package/dist/tasks/analyze.d.ts.map +1 -1
  430. package/dist/tasks/analyze.js +3 -2
  431. package/dist/tasks/analyze.js.map +1 -1
  432. package/dist/tasks/backfill-child-projections.js +47 -6
  433. package/dist/tasks/backfill-child-projections.js.map +1 -1
  434. package/dist/tasks/complete.d.ts.map +1 -1
  435. package/dist/tasks/complete.js +6 -1
  436. package/dist/tasks/complete.js.map +1 -1
  437. package/dist/tasks/completion-evaluation.d.ts.map +1 -1
  438. package/dist/tasks/completion-evaluation.js +3 -0
  439. package/dist/tasks/completion-evaluation.js.map +1 -1
  440. package/dist/tasks/evidence.d.ts +19 -1
  441. package/dist/tasks/evidence.d.ts.map +1 -1
  442. package/dist/tasks/evidence.js +93 -21
  443. package/dist/tasks/evidence.js.map +1 -1
  444. package/dist/tasks/infer-add-params.js +1 -1
  445. package/dist/tasks/infer-add-params.js.map +1 -1
  446. package/dist/tasks/task-context.d.ts.map +1 -1
  447. package/dist/tasks/task-context.js +3 -1
  448. package/dist/tasks/task-context.js.map +1 -1
  449. package/dist/tasks/task-import.d.ts.map +1 -1
  450. package/dist/tasks/task-import.js +11 -3
  451. package/dist/tasks/task-import.js.map +1 -1
  452. package/dist/tasks/task-next.d.ts +5 -1
  453. package/dist/tasks/task-next.d.ts.map +1 -1
  454. package/dist/tasks/task-next.js +7 -2
  455. package/dist/tasks/task-next.js.map +1 -1
  456. package/dist/validation/compliance.d.ts +5 -1
  457. package/dist/validation/compliance.d.ts.map +1 -1
  458. package/dist/validation/compliance.js +24 -3
  459. package/dist/validation/compliance.js.map +1 -1
  460. package/dist/validation/engine-ops.d.ts.map +1 -1
  461. package/dist/validation/engine-ops.js +3 -2
  462. package/dist/validation/engine-ops.js.map +1 -1
  463. package/dist/validation/index.d.ts +1 -1
  464. package/dist/validation/index.d.ts.map +1 -1
  465. package/dist/validation/index.js +1 -1
  466. package/dist/validation/index.js.map +1 -1
  467. package/dist/validation/protocol-common.d.ts +47 -2
  468. package/dist/validation/protocol-common.d.ts.map +1 -1
  469. package/dist/validation/protocol-common.js +86 -21
  470. package/dist/validation/protocol-common.js.map +1 -1
  471. package/dist/workgraph/scaffold-apply.d.ts.map +1 -1
  472. package/dist/workgraph/scaffold-apply.js +14 -5
  473. package/dist/workgraph/scaffold-apply.js.map +1 -1
  474. package/migrations/README.md +12 -0
  475. package/migrations/drizzle-cleo-project/20260928120000_t12341-row-uids/migration.sql +169 -0
  476. package/migrations/drizzle-cleo-project/20260930160000_t12886-dependency-cycle-guard/migration.sql +76 -0
  477. package/migrations/drizzle-cleo-project/20260930160000_t12886-dependency-cycle-guard/revert.sql +7 -0
  478. package/migrations/sync-journal/20260929140000_t12342-sync-clock/migration.sql +42 -0
  479. package/package.json +13 -13
  480. package/templates/CLEO-INJECTION.md +2 -2
  481. package/templates/CLEO-REFERENCE.md +5 -1
@@ -0,0 +1,1538 @@
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 { getTableColumns } from 'drizzle-orm';
74
+ import { CleoError } from '../errors.js';
75
+ import { exceedsMaxDepth } from '../tasks/hierarchy.js';
76
+ import { resolveHierarchyPolicy } from '../tasks/hierarchy-policy.js';
77
+ import { AC_UID_GRAVEYARD, BIRTH_FP_COLUMN, fillTableUids, markRowIdentityShared, mintRowUid, naturalRowUid, ROW_IDENTITY, registerRowUidFunction, rekeyedChildUid, rowIdentitySpec, UID_COLUMN, } from './row-identity.js';
78
+ import { advanceTaskIdSequence, clearRowUidNative, deleteDisplayIdAliasNative, deleteQuarantineNative, insertDisplayIdAliasNative, insertIdentityRowNative, insertQuarantineNative, insertUidAliasNative, rekeyQuarantineNative, renameTaskDisplayIdNative, rewriteStoredRefUidNative, setNaturalUidNative, setRowUidNative, } from './sqlite-data-accessor.js';
79
+ import { compareHlc as compareSyncHlc, encodeHlc, parseHlc } from './sync/hlc.js';
80
+ import { tasks as tasksTable } from './tasks-schema.js';
81
+ import { insertTaskSchema } from './validation-schemas.js';
82
+ /** Physical name of the display-id alias table. */
83
+ export const DISPLAY_ID_ALIAS_TABLE = 'tasks_display_id_aliases';
84
+ /** Physical name of the uid re-key alias table. */
85
+ export const UID_ALIAS_TABLE = 'tasks_uid_aliases';
86
+ /** Physical name of the identity quarantine (local-only). */
87
+ export const QUARANTINE_TABLE = 'tasks_identity_quarantine';
88
+ // ---- HLC -------------------------------------------------------------------
89
+ //
90
+ // HLC values are the change journal's (T12342): the wire string `Hlc` in
91
+ // `@cleocode/contracts` (`PPPPPPPPPPPPP-CCCCCC-<replica uuid>`), decoded and
92
+ // ordered by `store/sync/hlc.ts`. String order is HLC order.
93
+ /**
94
+ * Total order of two encoded HLC values (`Hlc` wire strings).
95
+ *
96
+ * @param a - One value.
97
+ * @param b - The other.
98
+ * @returns Negative, zero or positive.
99
+ * @throws HlcError when a value is not an encoded HLC.
100
+ */
101
+ export function compareHlc(a, b) {
102
+ return compareSyncHlc(storedHlc(a), storedHlc(b));
103
+ }
104
+ /** Physical milliseconds of an encoded HLC. */
105
+ function hlcMs(value) {
106
+ return storedHlc(value).phys;
107
+ }
108
+ /** The format 9.25 wrote with the uid flag on: `<ms 15>.<counter 6>.<node>`. */
109
+ const LEGACY_HLC_RE = /^(\d{15})\.(\d{6})\.(.+)$/;
110
+ /**
111
+ * Decode an HLC this module stored. The journal format is the only one
112
+ * written; a value in the 9.25 format (written only with
113
+ * `CLEO_ROW_UID_FILL=1`) is still read, ordered by the same
114
+ * `(physical, counter, node)` tuple, so an old alias never breaks a re-mint.
115
+ *
116
+ * @throws HlcError when the value is in neither format.
117
+ */
118
+ function storedHlc(value) {
119
+ const legacy = LEGACY_HLC_RE.exec(value);
120
+ if (legacy) {
121
+ return { phys: Number(legacy[1]), ctr: Number(legacy[2]), replica: legacy[3] };
122
+ }
123
+ return parseHlc(value);
124
+ }
125
+ /**
126
+ * Whether an incoming alias row ranks before the stored one of the same uid
127
+ * (T12800): the least displacement HLC by HLC order (a missing one ranks
128
+ * last), then origin, then `created_at`. The same on every replica.
129
+ */
130
+ function aliasPrecedes(incoming, stored) {
131
+ const a = incoming.displacedHlc;
132
+ const b = stored.displacedHlc;
133
+ if (a !== null && b === null)
134
+ return true;
135
+ if (a === null && b !== null)
136
+ return false;
137
+ if (a !== null && b !== null) {
138
+ const order = compareHlc(a, b);
139
+ if (order !== 0)
140
+ return order < 0;
141
+ }
142
+ const ao = incoming.origin ?? '';
143
+ const bo = stored.origin ?? '';
144
+ if (ao !== bo)
145
+ return ao < bo;
146
+ return incoming.createdAt < stored.createdAt;
147
+ }
148
+ /** The replica id a pre-HLC value carries: none issued it. */
149
+ export const PRE_HLC_REPLICA = '00000000-0000-0000-0000-000000000000';
150
+ /**
151
+ * The collision HLC of two rows that have no creation HLC (written before the
152
+ * change journal, T12342): the later of their two BIRTHS, at counter 0, from
153
+ * no replica ({@link PRE_HLC_REPLICA}). Every replica derives the same value
154
+ * from the rows' synced births; it only anchors the authority schedule
155
+ * ({@link remintAuthority}), never a clock (HLC never reads a uid's time).
156
+ *
157
+ * @param birthMsA - One row's birth, epoch ms.
158
+ * @param birthMsB - The other's.
159
+ * @returns An encoded HLC.
160
+ * @task T12802
161
+ */
162
+ export function collisionHlcFromBirths(birthMsA, birthMsB) {
163
+ return encodeHlc({ phys: Math.max(birthMsA, birthMsB, 0), ctr: 0, replica: PRE_HLC_REPLICA });
164
+ }
165
+ // ---- Shared helpers ---------------------------------------------------------
166
+ /** Quote an identifier for SQL. */
167
+ function q(name) {
168
+ return `"${name.replaceAll('"', '""')}"`;
169
+ }
170
+ /** The declared project spec of `table`, or throw. */
171
+ function specOf(table) {
172
+ const spec = rowIdentitySpec('project', table);
173
+ if (!spec)
174
+ throw new Error(`row identity: ${table} is not a declared project table`);
175
+ return spec;
176
+ }
177
+ /** The single-column key of a declared table. */
178
+ function keyOf(table) {
179
+ const spec = specOf(table);
180
+ if (spec.key.length !== 1)
181
+ throw new Error(`row identity: ${table} has a composite key`);
182
+ return spec.key[0];
183
+ }
184
+ /** Run `fn` in a uniquely named savepoint. */
185
+ function inSavepoint(db, name, fn) {
186
+ const sp = `_cleo_${name}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
187
+ db.exec(`SAVEPOINT ${sp}`);
188
+ try {
189
+ const out = fn();
190
+ db.exec(`RELEASE SAVEPOINT ${sp}`);
191
+ return out;
192
+ }
193
+ catch (error) {
194
+ db.exec(`ROLLBACK TO SAVEPOINT ${sp}`);
195
+ db.exec(`RELEASE SAVEPOINT ${sp}`);
196
+ throw error;
197
+ }
198
+ }
199
+ /** The local row of a minted table with this uid AND birth fingerprint. */
200
+ function localRow(db, table, uid, birthFp) {
201
+ return db
202
+ .prepare(`SELECT ${q(keyOf(table))} AS key, rowid AS rowid FROM main.${q(table)}
203
+ WHERE ${q(UID_COLUMN)} = ? AND ${q(BIRTH_FP_COLUMN)} = ?`)
204
+ .get(uid, birthFp);
205
+ }
206
+ // ---- Display-id collision: loser and authority -------------------------------
207
+ /**
208
+ * Of two rows that carry the same display id, the one that must be re-minted:
209
+ * the greater uid. Every replica picks the same one for the same pair, and for
210
+ * UUIDv7 uids the older row keeps its id.
211
+ *
212
+ * @param a - Uid of one claimant.
213
+ * @param b - Uid of the other.
214
+ * @returns The uid to re-mint.
215
+ */
216
+ export function collisionLoser(a, b) {
217
+ return a.toLowerCase() > b.toLowerCase() ? a : b;
218
+ }
219
+ /** Default length of one authority window (HLC physical time). */
220
+ export const REMINT_TAKEOVER_MS = 72 * 60 * 60 * 1000;
221
+ /**
222
+ * Which replica may re-mint the losing row of a display-id collision at HLC
223
+ * time `atHlc` (spec §9.2). A pure function of synced inputs: every replica
224
+ * evaluating the same time names the same authority.
225
+ *
226
+ * 1. The project syncs to cloud → the server.
227
+ * 2. Else the origin, while it is an active member, for the first window
228
+ * after the collision.
229
+ * 3. Else the FALLBACK CHAIN: the active members other than the origin,
230
+ * sorted, one window each, cycling. A fallback that stays silent loses its
231
+ * turn when its window ends, so a collision never waits forever.
232
+ *
233
+ * @param input - The facts every replica can see.
234
+ * @returns The authority, the rule, and the window index (0 = origin).
235
+ */
236
+ export function remintAuthority(input) {
237
+ if (input.cloudSynced)
238
+ return { authority: 'server', reason: 'server', slot: 0 };
239
+ const window = input.takeoverAfterMs ?? REMINT_TAKEOVER_MS;
240
+ const elapsed = Math.max(0, hlcMs(input.atHlc) - hlcMs(input.collisionHlc));
241
+ const active = input.replicas
242
+ .filter((r) => compareHlc(r.joinedHlc, input.atHlc) <= 0 &&
243
+ (r.retiredHlc == null || compareHlc(r.retiredHlc, input.atHlc) > 0))
244
+ .map((r) => r.id)
245
+ .sort();
246
+ if (active.length === 0)
247
+ throw new Error('remint authority: the project has no active replica');
248
+ const originLive = input.origin !== null && active.includes(input.origin);
249
+ if (originLive && elapsed < window) {
250
+ return { authority: input.origin, reason: 'origin', slot: 0 };
251
+ }
252
+ const others = active.filter((id) => id !== input.origin);
253
+ const chain = others.length > 0 ? others : active;
254
+ const turn = Math.floor(elapsed / window) - (originLive ? 1 : 0);
255
+ return {
256
+ authority: chain[turn % chain.length],
257
+ reason: 'fallback',
258
+ slot: turn + 1,
259
+ };
260
+ }
261
+ /**
262
+ * Record that `displayId` used to name the row (`entityUid`, `entityBirthFp`).
263
+ * Idempotent.
264
+ *
265
+ * @param db - Connection on the project `cleo.db`.
266
+ * @param entry - The displaced id, the row that carried it, and the displacement.
267
+ */
268
+ export function recordDisplayIdAlias(db, entry) {
269
+ insertDisplayIdAliasNative(db, {
270
+ // The reason is part of the key (T12800 review): a `remint-assigned` and a
271
+ // `collision-remint` alias of one (id, row) are two facts, and neither may
272
+ // win an insert race and drop the other.
273
+ uid: naturalRowUid('project', DISPLAY_ID_ALIAS_TABLE, [
274
+ entry.table,
275
+ entry.displayId,
276
+ entry.entityUid,
277
+ entry.reason,
278
+ ]),
279
+ entityTable: entry.table,
280
+ displayId: entry.displayId,
281
+ entityUid: entry.entityUid,
282
+ entityBirthFp: entry.entityBirthFp,
283
+ reason: entry.reason,
284
+ origin: entry.origin ?? null,
285
+ displacedHlc: entry.displacedHlc ?? null,
286
+ // Derived from the displacement's HLC when there is one, so every replica
287
+ // that records this displacement writes the same row (T12800).
288
+ createdAt: entry.now ?? hlcTime(entry.displacedHlc) ?? new Date().toISOString(),
289
+ }, aliasPrecedes);
290
+ }
291
+ /** Wall-clock time of an encoded HLC, or `undefined` when there is none. */
292
+ function hlcTime(hlc) {
293
+ if (!hlc)
294
+ return undefined;
295
+ try {
296
+ return new Date(storedHlc(hlc).phys).toISOString();
297
+ }
298
+ catch {
299
+ return undefined;
300
+ }
301
+ }
302
+ /**
303
+ * Resolve a display id: a live row always wins (aliases come back as
304
+ * history); with no live row, one alias resolves and several are ambiguous.
305
+ *
306
+ * @param db - Connection on the project `cleo.db`.
307
+ * @param table - Display-id table (e.g. `tasks_tasks`).
308
+ * @param displayId - The id to resolve.
309
+ * @returns `resolved` (live, or the single alias), `ambiguous` with every
310
+ * candidate (callers report it as an error and never pick), or `none`.
311
+ */
312
+ export function resolveDisplayId(db, table, displayId) {
313
+ const spec = specOf(table);
314
+ if (!spec.displayId)
315
+ throw new Error(`display id: ${table} has no declared display id`);
316
+ const key = keyOf(table);
317
+ const live = db
318
+ .prepare(`SELECT ${q(UID_COLUMN)} AS uid FROM main.${q(table)} WHERE ${q(key)} = ?`)
319
+ .get(displayId);
320
+ const aliases = db
321
+ .prepare(`SELECT a.entity_uid AS uid, t.${q(key)} AS currentId, a.origin AS origin,
322
+ a.displaced_hlc AS displacedHlc
323
+ FROM ${DISPLAY_ID_ALIAS_TABLE} a
324
+ LEFT JOIN main.${q(table)} t
325
+ ON t.${q(UID_COLUMN)} = a.entity_uid
326
+ AND (a.entity_birth_fp IS NULL OR t.${q(BIRTH_FP_COLUMN)} = a.entity_birth_fp)
327
+ WHERE a.entity_table = ? AND a.display_id = ?
328
+ ORDER BY a.displaced_hlc, a.entity_uid`)
329
+ .all(table, displayId)
330
+ .filter((a) => a.uid !== live?.uid)
331
+ // One claimant per row: several reasons (collision-remint, remint-assigned)
332
+ // of one id and row are one history entry here.
333
+ .filter((a, i, all) => all.findIndex((b) => b.uid === a.uid) === i)
334
+ .map((a) => ({ ...a, via: 'alias' }));
335
+ if (live) {
336
+ return {
337
+ status: 'resolved',
338
+ claimant: { uid: live.uid, currentId: displayId, via: 'live' },
339
+ alsoKnownAs: aliases,
340
+ };
341
+ }
342
+ const [only] = aliases;
343
+ if (!only)
344
+ return { status: 'none' };
345
+ if (aliases.length > 1)
346
+ return { status: 'ambiguous', candidates: aliases };
347
+ return { status: 'resolved', claimant: only, alsoKnownAs: [] };
348
+ }
349
+ /**
350
+ * Allocate the next `T####` on the caller's handle, with the same lower bound
351
+ * `allocateNextTaskId` uses (the highest stored numeric id). Only the re-mint
352
+ * authority allocates for a collision.
353
+ *
354
+ * @param db - Connection on the project `cleo.db`.
355
+ * @returns The new display id.
356
+ */
357
+ export function allocateTaskDisplayId(db) {
358
+ const inventory = db
359
+ .prepare(`SELECT COALESCE(MAX(CAST(substr(id, 2) AS INTEGER)), 0) AS maximum
360
+ FROM tasks_tasks WHERE id GLOB 'T[0-9]*' AND substr(id, 2) NOT GLOB '*[^0-9]*'`)
361
+ .get();
362
+ const counter = advanceTaskIdSequence(db, inventory.maximum);
363
+ if (counter === undefined) {
364
+ throw new CleoError(ExitCode.FILE_ERROR, 'Sequence counter not found during allocation', {
365
+ fix: 'Run `cleo sequence repair` to re-seed the task-id sequence, then retry.',
366
+ });
367
+ }
368
+ return `T${String(counter).padStart(3, '0')}`;
369
+ }
370
+ /** Every local column that holds a task's display id. */
371
+ function taskReferenceColumns(db) {
372
+ const found = new Map();
373
+ const add = (table, column, jsonArray) => {
374
+ found.set(`${table}.${column}`, { table, column, jsonArray });
375
+ };
376
+ const fks = db
377
+ .prepare(`SELECT m.name AS tbl, f."from" AS col
378
+ FROM main.sqlite_master m, pragma_foreign_key_list(m.name) f
379
+ WHERE m.type = 'table' AND f."table" = 'tasks_tasks'
380
+ AND (f."to" IS NULL OR f."to" = 'id')`)
381
+ .all();
382
+ for (const fk of fks)
383
+ add(fk.tbl, fk.col, false);
384
+ for (const spec of ROW_IDENTITY.project) {
385
+ for (const ref of [...(spec.refs ?? []), ...(spec.keyRefs ?? []), ...(spec.owners ?? [])]) {
386
+ if (ref.table === 'tasks_tasks')
387
+ add(spec.table, ref.column, false);
388
+ }
389
+ for (const ref of spec.jsonArrayRefs ?? []) {
390
+ if (ref.table === 'tasks_tasks')
391
+ add(spec.table, ref.column, true);
392
+ }
393
+ }
394
+ const tables = new Set(db.prepare("SELECT name FROM main.sqlite_master WHERE type = 'table'").all().map((r) => r.name));
395
+ return [...found.values()].filter((ref) => tables.has(ref.table));
396
+ }
397
+ function readRemint(db, uid, birthFp) {
398
+ const rows = db
399
+ .prepare(`SELECT display_id AS newId, displaced_hlc AS hlc, origin FROM ${DISPLAY_ID_ALIAS_TABLE}
400
+ WHERE entity_table = 'tasks_tasks' AND entity_uid = ? AND entity_birth_fp = ?
401
+ AND reason = 'remint-assigned' AND displaced_hlc IS NOT NULL`)
402
+ .all(uid, birthFp);
403
+ let best;
404
+ for (const r of rows)
405
+ if (!best || compareHlc(r.hlc, best.hlc) > 0)
406
+ best = r;
407
+ return best;
408
+ }
409
+ /** Record the display id an op assigned (every op, winner or not: its number stays an alias). */
410
+ function writeRemint(db, op) {
411
+ recordDisplayIdAlias(db, {
412
+ table: 'tasks_tasks',
413
+ displayId: op.newId,
414
+ entityUid: op.uid,
415
+ entityBirthFp: op.birthFp,
416
+ reason: 'remint-assigned',
417
+ origin: op.origin,
418
+ displacedHlc: op.hlc,
419
+ });
420
+ }
421
+ /**
422
+ * AUTHORITY ONLY ({@link remintAuthority}): give the losing task of a
423
+ * display-id collision a new display id. Allocates the next `T####`, renames
424
+ * through the chokepoint (version CAS, claim columns kept), records the
425
+ * contested id as an alias and the op as the row's re-mint record, places any
426
+ * held row the freed id unblocks, and returns the op to publish.
427
+ *
428
+ * @param db - Connection on the project `cleo.db`.
429
+ * @param taskId - Current local display id of the task to re-mint.
430
+ * @param options - The contested id (default: `taskId`), why, the authority,
431
+ * the op's HLC, and an optional version / claim guard for a manual rename.
432
+ * @returns The op to publish, with what was rewritten here.
433
+ * @throws CleoError NOT_FOUND when the task does not exist or has no identity yet.
434
+ */
435
+ export function remintTaskDisplayId(db, taskId, options) {
436
+ const row = db
437
+ .prepare('SELECT uid, birth_fp AS birthFp FROM tasks_tasks WHERE id = ?')
438
+ .get(taskId);
439
+ if (!row?.uid || !row.birthFp) {
440
+ throw new CleoError(ExitCode.NOT_FOUND, `No task ${taskId} with a uid and birth fingerprint`, {
441
+ fix: 'Check the id with `cleo show`; row identity is filled when CLEO_ROW_UID_FILL=1.',
442
+ details: { field: 'taskId', actual: taskId },
443
+ });
444
+ }
445
+ const { uid, birthFp } = row;
446
+ const oldId = options.contestedId ?? taskId;
447
+ registerRowUidFunction(db, 'project');
448
+ return inSavepoint(db, 'remint', () => {
449
+ const newId = allocateTaskDisplayId(db);
450
+ const { rewritten } = renameTaskDisplayIdNative(db, uid, newId, taskReferenceColumns(db), options.guard);
451
+ recordDisplayIdAlias(db, {
452
+ table: 'tasks_tasks',
453
+ displayId: oldId,
454
+ entityUid: uid,
455
+ entityBirthFp: birthFp,
456
+ reason: options.reason,
457
+ origin: options.origin,
458
+ displacedHlc: options.hlc,
459
+ now: options.now,
460
+ });
461
+ const op = { uid, birthFp, oldId, newId, origin: options.origin, hlc: options.hlc };
462
+ writeRemint(db, op);
463
+ const released = releaseHeldRows(db, [
464
+ { table: 'tasks_tasks', key: taskId },
465
+ { table: 'tasks_tasks', key: oldId },
466
+ ]);
467
+ return { ...op, rewritten, released };
468
+ });
469
+ }
470
+ /**
471
+ * NON-AUTHORITY: apply a re-mint an authority published. Of several ops for
472
+ * one row, the one with the greatest HLC wins on every replica, whatever the
473
+ * arrival order: a lesser op is recorded as an alias (`superseded`), a greater
474
+ * one renames the row again and turns the previous number into an alias.
475
+ * Never allocates.
476
+ *
477
+ * @param db - Connection on the project `cleo.db`.
478
+ * @param op - The published re-mint.
479
+ * @returns What happened.
480
+ */
481
+ export function applyRemintOp(db, op) {
482
+ registerRowUidFunction(db, 'project');
483
+ markRowIdentityShared(db, 'receive');
484
+ const prior = readRemint(db, op.uid, op.birthFp);
485
+ const alias = (displayId, reason) => recordDisplayIdAlias(db, {
486
+ table: 'tasks_tasks',
487
+ displayId,
488
+ entityUid: op.uid,
489
+ entityBirthFp: op.birthFp,
490
+ reason,
491
+ origin: op.origin,
492
+ displacedHlc: op.hlc,
493
+ });
494
+ if (prior) {
495
+ const order = compareHlc(op.hlc, prior.hlc);
496
+ if (order === 0)
497
+ return { status: 'already-applied' };
498
+ if (order < 0) {
499
+ inSavepoint(db, 'remint_superseded', () => {
500
+ alias(op.oldId, 'collision-remint');
501
+ writeRemint(db, op);
502
+ });
503
+ return { status: 'superseded', winner: prior };
504
+ }
505
+ }
506
+ const live = localRow(db, 'tasks_tasks', op.uid, op.birthFp);
507
+ if (live) {
508
+ const holder = db.prepare('SELECT uid FROM tasks_tasks WHERE id = ?').get(op.newId);
509
+ if (holder && holder.uid !== op.uid)
510
+ return { status: 'conflict', holderUid: holder.uid };
511
+ }
512
+ return inSavepoint(db, 'remint_apply', () => {
513
+ alias(op.oldId, 'collision-remint');
514
+ writeRemint(db, op);
515
+ if (!live) {
516
+ const held = { entityTable: 'tasks_tasks', uid: op.uid, birthFp: op.birthFp };
517
+ return {
518
+ status: 'recorded',
519
+ released: releaseHeldRows(db, [{ table: 'tasks_tasks', held }]),
520
+ };
521
+ }
522
+ if (live.key === op.newId)
523
+ return { status: 'already-applied' };
524
+ const { rewritten } = renameTaskDisplayIdNative(db, op.uid, op.newId, taskReferenceColumns(db));
525
+ return {
526
+ status: 'applied',
527
+ receipt: {
528
+ ...op,
529
+ rewritten,
530
+ released: releaseHeldRows(db, [{ table: 'tasks_tasks', key: live.key }]),
531
+ },
532
+ };
533
+ });
534
+ }
535
+ /**
536
+ * Version of the wire row format. v2 (T12798) carries no local key of
537
+ * another row in `values`; a row without it (or older) came from a build
538
+ * whose values hold raw local keys, and is held (`unsupported-wire`) rather
539
+ * than applied or silently stripped.
540
+ */
541
+ export const WIRE_VERSION = 2;
542
+ /**
543
+ * AC-history rows whose criterion is gone everywhere are still history: they
544
+ * are placed with the criterion's uid carried and a key that names no live
545
+ * criterion (`gone:<uid>`), never held forever.
546
+ */
547
+ const GONE_CRITERION_PREFIX = 'gone:';
548
+ /** Follow `tasks_uid_aliases` from (uid, birth fp) to the row's current uid. */
549
+ function followUidAlias(db, table, uid, birthFp) {
550
+ let current = uid;
551
+ for (let hop = 0; hop < 32; hop++) {
552
+ const alias = db
553
+ .prepare(`SELECT new_uid AS uid FROM ${UID_ALIAS_TABLE}
554
+ WHERE entity_table = ? AND old_uid = ? AND old_birth_fp = ?`)
555
+ .get(table, current, birthFp);
556
+ if (!alias)
557
+ break;
558
+ current = alias.uid;
559
+ }
560
+ return current;
561
+ }
562
+ /**
563
+ * Resolve a wire reference to a local row: the row with that uid AND that
564
+ * birth fingerprint, following `tasks_uid_aliases` when the target was
565
+ * re-keyed. A row that has the uid with ANOTHER fingerprint (the other side of
566
+ * a uid collision) never matches: the reference is `held` (its target waits
567
+ * in the quarantine) or `pending` (its target has not arrived, or its re-key
568
+ * has not), and the referencing row waits too.
569
+ *
570
+ * @param db - Connection on the project `cleo.db`.
571
+ * @param table - The referenced table.
572
+ * @param ref - The reference.
573
+ * @returns Where it points.
574
+ */
575
+ export function resolveWireRef(db, table, ref) {
576
+ if (ref.uid === null)
577
+ return { status: 'pending' };
578
+ const minted = specOf(table).kind === 'minted';
579
+ // A minted row is identified by uid AND fingerprint: a reference without a
580
+ // fingerprint never matches one (the other side of a uid collision holds the
581
+ // same uid).
582
+ if (minted && ref.birthFp === null)
583
+ return { status: 'pending' };
584
+ const uid = minted && ref.birthFp !== null ? followUidAlias(db, table, ref.uid, ref.birthFp) : ref.uid;
585
+ const row = db
586
+ .prepare(`SELECT ${q(keyOf(table))} AS key, ${minted ? q(BIRTH_FP_COLUMN) : 'NULL'} AS fp
587
+ FROM main.${q(table)} WHERE ${q(UID_COLUMN)} = ?`)
588
+ .get(uid);
589
+ if (row && (!minted || row.fp === ref.birthFp)) {
590
+ return { status: 'row', key: row.key, uid };
591
+ }
592
+ const held = db
593
+ .prepare(`SELECT 1 AS x FROM ${QUARANTINE_TABLE} WHERE entity_table = ? AND uid = ? AND birth_fp = ?`)
594
+ .get(table, uid, ref.birthFp ?? '');
595
+ return held ? { status: 'held' } : { status: 'pending' };
596
+ }
597
+ /** Whether a column accepts NULL. */
598
+ function nullableColumn(db, table, column) {
599
+ const info = db
600
+ .prepare('SELECT "notnull" AS notnull, pk FROM pragma_table_info(?) WHERE name = ?')
601
+ .get(table, column);
602
+ return info !== undefined && info.notnull === 0 && info.pk === 0;
603
+ }
604
+ /** Reference column → referenced table, for a declared table. */
605
+ function refTargets(spec) {
606
+ const out = new Map();
607
+ for (const ref of [...(spec.refs ?? []), ...(spec.owners ?? []), ...(spec.keyRefs ?? [])]) {
608
+ out.set(ref.column, ref.table);
609
+ }
610
+ return out;
611
+ }
612
+ /** Stored reference uid column (`ac_uid`) → its table and key column (`ac_id`). */
613
+ function storedUidRefs(spec) {
614
+ const out = new Map();
615
+ for (const ref of spec.storedRefUids ?? []) {
616
+ if ((ref.source ?? 'uid') === 'uid')
617
+ out.set(ref.column, { table: ref.table, from: ref.from });
618
+ }
619
+ return out;
620
+ }
621
+ /** JSON array of ids column → referenced table. */
622
+ function jsonArrayTargets(spec) {
623
+ return new Map((spec.jsonArrayRefs ?? []).map((ref) => [ref.column, ref.table]));
624
+ }
625
+ /**
626
+ * Columns that hold another row's LOCAL key: never sent as values (T12798).
627
+ * `ac_text_hash` (a stored fact, not a key) is kept.
628
+ */
629
+ function localKeyColumns(spec) {
630
+ const out = new Set([...refTargets(spec).keys(), ...jsonArrayTargets(spec).keys()]);
631
+ for (const [column, { from }] of storedUidRefs(spec)) {
632
+ out.add(column);
633
+ out.add(from);
634
+ }
635
+ return out;
636
+ }
637
+ /** Hold a row, keeping the time it was first held. */
638
+ function hold(db, wire, reason, contestedId, previous) {
639
+ const key = { entityTable: wire.table, uid: wire.uid, birthFp: wire.birthFp ?? '' };
640
+ const was = previous ?? key;
641
+ const rowJson = JSON.stringify(wire);
642
+ const first = db
643
+ .prepare(`SELECT created_at AS createdAt, reason, contested_id AS contestedId, row_json AS rowJson
644
+ FROM ${QUARANTINE_TABLE} WHERE entity_table = ? AND uid = ? AND birth_fp = ?`)
645
+ .get(was.entityTable, was.uid, was.birthFp);
646
+ // A still-held row that is held for the same reason is never rewritten (T12801).
647
+ if (first &&
648
+ was.entityTable === key.entityTable &&
649
+ was.uid === key.uid &&
650
+ was.birthFp === key.birthFp &&
651
+ first.reason === reason &&
652
+ first.contestedId === contestedId &&
653
+ first.rowJson === rowJson) {
654
+ return;
655
+ }
656
+ if (previous)
657
+ deleteQuarantineNative(db, previous);
658
+ insertQuarantineNative(db, {
659
+ ...key,
660
+ reason,
661
+ contestedId,
662
+ rowJson,
663
+ receivedHlc: null,
664
+ createdAt: first?.createdAt ?? new Date().toISOString(),
665
+ });
666
+ }
667
+ /** Place one wire row, or hold it. Does not re-try other held rows. */
668
+ function placeRow(db, incoming, heldAs) {
669
+ const spec = specOf(incoming.table);
670
+ const minted = spec.kind === 'minted';
671
+ // A v1 sender's values hold raw local keys of other rows: recognised and
672
+ // held, never applied and never silently stripped (T12798 review).
673
+ const version = incoming.version ?? 1;
674
+ if (version !== WIRE_VERSION) {
675
+ hold(db, incoming, 'unsupported-wire', `wire v${version}`, heldAs);
676
+ return { status: 'held', reason: 'unsupported-wire' };
677
+ }
678
+ if (minted && !incoming.birthFp) {
679
+ throw new Error(`receive: a ${incoming.table} row must carry its birth fingerprint`);
680
+ }
681
+ // A late copy of a row that was re-keyed: follow its uid alias.
682
+ const wire = minted
683
+ ? {
684
+ ...incoming,
685
+ uid: followUidAlias(db, incoming.table, incoming.uid, incoming.birthFp),
686
+ }
687
+ : incoming;
688
+ const done = () => {
689
+ if (heldAs)
690
+ deleteQuarantineNative(db, heldAs);
691
+ };
692
+ // A natural row is matched by its key AFTER its references resolve (below):
693
+ // its wire uid hashes endpoint uids, and an endpoint under a uid collision
694
+ // would make two different edges look like one.
695
+ const same = minted
696
+ ? db
697
+ .prepare(`SELECT ${q(BIRTH_FP_COLUMN)} AS fp FROM main.${q(wire.table)} WHERE ${q(UID_COLUMN)} = ?`)
698
+ .get(wire.uid)
699
+ : undefined;
700
+ if (same) {
701
+ if (same.fp === wire.birthFp) {
702
+ done();
703
+ return { status: 'duplicate', uid: wire.uid };
704
+ }
705
+ const local = same.fp ?? '';
706
+ const other = wire.birthFp;
707
+ const localIsLoser = local > other;
708
+ hold(db, wire, 'uid-collision', null, heldAs);
709
+ return {
710
+ status: 'held',
711
+ reason: 'uid-collision',
712
+ collision: {
713
+ kind: 'uid',
714
+ table: wire.table,
715
+ uid: wire.uid,
716
+ winnerBirthFp: localIsLoser ? other : local,
717
+ loserBirthFp: localIsLoser ? local : other,
718
+ localIsLoser,
719
+ },
720
+ };
721
+ }
722
+ const values = { ...wire.values };
723
+ // A local key of another row never comes from the sender's values (T12798);
724
+ // a JSON column that is not an id array travels as a value.
725
+ const arrayColumns = jsonArrayTargets(spec);
726
+ for (const column of localKeyColumns(spec)) {
727
+ if (!arrayColumns.has(column) || wire.jsonRefs?.[column])
728
+ delete values[column];
729
+ }
730
+ const targets = refTargets(spec);
731
+ const stored = storedUidRefs(spec);
732
+ const pending = () => {
733
+ hold(db, wire, 'ref-pending', null, heldAs);
734
+ return { status: 'held', reason: 'ref-pending' };
735
+ };
736
+ for (const [column, ref] of Object.entries(wire.refs ?? {})) {
737
+ const storedRef = stored.get(column);
738
+ const target = targets.get(column) ?? storedRef?.table;
739
+ if (!target)
740
+ throw new Error(`receive: ${wire.table}.${column} is not a declared reference`);
741
+ if (ref === null) {
742
+ values[column] = null;
743
+ if (storedRef)
744
+ values[storedRef.from] = null;
745
+ continue;
746
+ }
747
+ const resolved = resolveWireRef(db, target, ref);
748
+ if (resolved.status !== 'row' && ref.gone) {
749
+ // The sender no longer has the row this names, and neither do we.
750
+ if (storedRef && wire.table === 'tasks_task_acceptance_criteria_history') {
751
+ // History of a criterion gone everywhere: keep the history, carry the
752
+ // criterion's uid, point the key at no live criterion.
753
+ values[column] = ref.uid;
754
+ values[storedRef.from] = `${GONE_CRITERION_PREFIX}${ref.uid ?? wire.uid}`;
755
+ continue;
756
+ }
757
+ if (!storedRef && nullableColumn(db, wire.table, column)) {
758
+ values[column] = null; // a dangling optional reference is dropped
759
+ continue;
760
+ }
761
+ }
762
+ if (resolved.status !== 'row')
763
+ return pending();
764
+ if (storedRef) {
765
+ values[storedRef.from] = resolved.key;
766
+ values[column] = resolved.uid;
767
+ }
768
+ else {
769
+ values[column] = resolved.key;
770
+ }
771
+ }
772
+ for (const [column, refs] of Object.entries(wire.jsonRefs ?? {})) {
773
+ const target = arrayColumns.get(column);
774
+ if (!target)
775
+ throw new Error(`receive: ${wire.table}.${column} is not a declared id array`);
776
+ const keys = [];
777
+ for (const ref of refs) {
778
+ const resolved = resolveWireRef(db, target, ref);
779
+ if (resolved.status !== 'row')
780
+ return pending();
781
+ keys.push(resolved.key);
782
+ }
783
+ values[column] = JSON.stringify(keys);
784
+ }
785
+ if (spec.displayId && minted) {
786
+ const key = keyOf(wire.table);
787
+ const remint = readRemint(db, wire.uid, wire.birthFp);
788
+ if (remint)
789
+ values[key] = remint.newId;
790
+ const holder = db
791
+ .prepare(`SELECT ${q(UID_COLUMN)} AS uid FROM main.${q(wire.table)} WHERE ${q(key)} = ?`)
792
+ .get(values[key] ?? null);
793
+ if (holder) {
794
+ const contested = String(values[key]);
795
+ const loserUid = collisionLoser(holder.uid ?? '', wire.uid);
796
+ hold(db, wire, 'display-id-collision', contested, heldAs);
797
+ return {
798
+ status: 'held',
799
+ reason: 'display-id-collision',
800
+ collision: {
801
+ kind: 'display-id',
802
+ table: wire.table,
803
+ displayId: contested,
804
+ loserUid,
805
+ localIsLoser: loserUid === holder.uid,
806
+ },
807
+ };
808
+ }
809
+ }
810
+ if (wire.table === DISPLAY_ID_ALIAS_TABLE || wire.table === UID_ALIAS_TABLE) {
811
+ // An alias row's uid IS its primary key, computed on write from its key
812
+ // values (none is a reference): derive it the same way here (T12800).
813
+ const uid = naturalRowUid('project', wire.table, spec.key.map((k) => (values[k] ?? null)));
814
+ const existed = db.prepare(`SELECT 1 AS x FROM main.${q(wire.table)} WHERE ${q(UID_COLUMN)} = ?`).get(uid) !==
815
+ undefined;
816
+ if (wire.table === DISPLAY_ID_ALIAS_TABLE) {
817
+ // The order-independent upsert: whichever copy arrives first, every
818
+ // replica keeps the same row.
819
+ insertDisplayIdAliasNative(db, {
820
+ uid,
821
+ entityTable: String(values['entity_table']),
822
+ displayId: String(values['display_id']),
823
+ entityUid: String(values['entity_uid']),
824
+ entityBirthFp: values['entity_birth_fp'] ?? null,
825
+ reason: String(values['reason']),
826
+ origin: values['origin'] ?? null,
827
+ displacedHlc: values['displaced_hlc'] ?? null,
828
+ createdAt: String(values['created_at']),
829
+ }, aliasPrecedes);
830
+ }
831
+ else if (!existed) {
832
+ insertIdentityRowNative(db, wire.table, { ...values, [UID_COLUMN]: uid });
833
+ }
834
+ done();
835
+ return existed ? { status: 'duplicate', uid } : { status: 'inserted', key: uid, uid };
836
+ }
837
+ if (!minted) {
838
+ const existing = db
839
+ .prepare(`SELECT ${q(UID_COLUMN)} AS uid FROM main.${q(wire.table)}
840
+ WHERE ${spec.key.map((k) => `${q(k)} = ?`).join(' AND ')}`)
841
+ .get(...spec.key.map((k) => values[k] ?? null));
842
+ if (existing) {
843
+ done();
844
+ return { status: 'duplicate', uid: existing.uid ?? wire.uid };
845
+ }
846
+ }
847
+ const rowidKey = rowidKeyOf(db, wire.table);
848
+ // An INTEGER PRIMARY KEY (AUTOINCREMENT) numbers from 1 on every device: the
849
+ // sender's value means nothing here. Drop it; SQLite assigns a local one (T12799).
850
+ if (rowidKey)
851
+ delete values[rowidKey];
852
+ if (!spec.displayId && spec.key.length === 1 && !rowidKey) {
853
+ // A local key another row holds (keys such as AC ids derive from local
854
+ // display ids): the merge engine re-derives it (T12344); held until then.
855
+ const key = spec.key[0];
856
+ const taken = db
857
+ .prepare(`SELECT 1 AS x FROM main.${q(wire.table)} WHERE ${q(key)} = ?`)
858
+ .get(values[key] ?? null);
859
+ if (taken) {
860
+ hold(db, wire, 'key-collision', String(values[key]), heldAs);
861
+ return { status: 'held', reason: 'key-collision' };
862
+ }
863
+ }
864
+ // Validate like a local writer would (T12801): a received row is data from
865
+ // another device, never trusted to satisfy this store's invariants.
866
+ const invalid = validateReceived(db, wire.table, values);
867
+ if (invalid) {
868
+ hold(db, wire, 'invalid', invalid, heldAs);
869
+ return { status: 'held', reason: 'invalid' };
870
+ }
871
+ try {
872
+ inSavepoint(db, 'receive_insert', () => {
873
+ if (minted) {
874
+ insertIdentityRowNative(db, wire.table, {
875
+ ...values,
876
+ [UID_COLUMN]: wire.uid,
877
+ [BIRTH_FP_COLUMN]: wire.birthFp,
878
+ });
879
+ }
880
+ else {
881
+ // A natural uid is a pure function of its endpoints: derive it here.
882
+ insertIdentityRowNative(db, wire.table, { ...values, [UID_COLUMN]: null });
883
+ fillTableUids(db, 'project', wire.table);
884
+ }
885
+ });
886
+ }
887
+ catch (error) {
888
+ // The store's own guards refused it (hierarchy cycle and type-matrix
889
+ // triggers, CHECK and NOT NULL constraints): hold it, never throw the
890
+ // merge away, never insert it.
891
+ if (!isConstraintError(error))
892
+ throw error;
893
+ const message = error instanceof Error ? error.message : String(error);
894
+ hold(db, wire, 'invalid', message.slice(0, 200), heldAs);
895
+ return { status: 'held', reason: 'invalid' };
896
+ }
897
+ done();
898
+ const key = rowidKey
899
+ ? String(db
900
+ .prepare(`SELECT ${q(rowidKey)} AS k FROM main.${q(wire.table)} WHERE ${q(UID_COLUMN)} = ?`)
901
+ .get(wire.uid).k)
902
+ : spec.key.length === 1
903
+ ? String(values[spec.key[0]])
904
+ : wire.uid;
905
+ return { status: 'inserted', key, uid: wire.uid };
906
+ }
907
+ /** The table's INTEGER PRIMARY KEY column (a rowid alias, locally numbered), if it has one. */
908
+ function rowidKeyOf(db, table) {
909
+ const pk = db.prepare('SELECT name, type, pk FROM pragma_table_info(?)').all(table).filter((c) => c.pk > 0);
910
+ const [only] = pk;
911
+ return pk.length === 1 && only && only.type.toUpperCase() === 'INTEGER' ? only.name : undefined;
912
+ }
913
+ /** Held rows a trigger may unblock (indexed lookups and a JSON text match, never a full retry). */
914
+ function candidatesFor(db, t) {
915
+ const cols = `entity_table AS entityTable, uid, birth_fp AS birthFp, row_json AS rowJson`;
916
+ const out = [];
917
+ if (t.held) {
918
+ const row = db
919
+ .prepare(`SELECT ${cols} FROM ${QUARANTINE_TABLE} WHERE entity_table = ? AND uid = ? AND birth_fp = ?`)
920
+ .get(t.held.entityTable, t.held.uid, t.held.birthFp);
921
+ if (row)
922
+ out.push(row);
923
+ }
924
+ if (t.key !== undefined) {
925
+ out.push(...db
926
+ .prepare(`SELECT ${cols} FROM ${QUARANTINE_TABLE} WHERE entity_table = ? AND contested_id = ?
927
+ ORDER BY created_at, uid`)
928
+ .all(t.table, t.key));
929
+ }
930
+ if (t.uid !== undefined) {
931
+ // Rows waiting on a reference to this uid carry it in their wire form,
932
+ // either as this uid or as an old uid a re-key led here.
933
+ const uids = new Set([t.uid]);
934
+ for (let frontier = [t.uid]; frontier.length > 0;) {
935
+ const older = db
936
+ .prepare(`SELECT old_uid AS uid FROM ${UID_ALIAS_TABLE}
937
+ WHERE entity_table = ? AND new_uid IN (${frontier.map(() => '?').join(', ')})`)
938
+ .all(t.table, ...frontier);
939
+ frontier = older.map((r) => r.uid).filter((u) => !uids.has(u));
940
+ for (const u of frontier)
941
+ uids.add(u);
942
+ }
943
+ const find = db.prepare(`SELECT ${cols} FROM ${QUARANTINE_TABLE} WHERE instr(row_json, ?) > 0 AND uid <> ?
944
+ ORDER BY created_at, uid`);
945
+ for (const uid of uids) {
946
+ out.push(...find.all(`"${uid}"`, t.uid));
947
+ }
948
+ }
949
+ return out;
950
+ }
951
+ /**
952
+ * Re-try the held rows the triggers may unblock, then the rows each placement
953
+ * unblocks in turn (a placed owner unblocks its children). A held row is only
954
+ * re-tried when something it may wait on changed, and a row still held for
955
+ * the same reason is not rewritten (T12801: no full re-scan per receive).
956
+ * `'all'` re-tries every held row once (explicit {@link releaseHeld}).
957
+ */
958
+ function releaseHeldRows(db, triggers) {
959
+ const released = [];
960
+ const work = [];
961
+ const attempt = (h) => {
962
+ const key = { entityTable: h.entityTable, uid: h.uid, birthFp: h.birthFp };
963
+ const result = placeRow(db, JSON.parse(h.rowJson), key);
964
+ if (result.status === 'held')
965
+ return;
966
+ released.push(key);
967
+ if (result.status === 'inserted') {
968
+ work.push({ table: h.entityTable, uid: result.uid, key: result.key });
969
+ }
970
+ };
971
+ if (triggers === 'all') {
972
+ const held = db
973
+ .prepare(`SELECT entity_table AS entityTable, uid, birth_fp AS birthFp, row_json AS rowJson
974
+ FROM ${QUARANTINE_TABLE} ORDER BY created_at, entity_table, uid`)
975
+ .all();
976
+ for (const h of held)
977
+ attempt(h);
978
+ }
979
+ else {
980
+ work.push(...triggers);
981
+ }
982
+ for (let next = work.shift(); next !== undefined; next = work.shift()) {
983
+ const seen = new Set();
984
+ for (const h of candidatesFor(db, next)) {
985
+ const id = `${h.entityTable}\u0000${h.uid}\u0000${h.birthFp}`;
986
+ if (seen.has(id))
987
+ continue;
988
+ seen.add(id);
989
+ attempt(h);
990
+ }
991
+ }
992
+ return released;
993
+ }
994
+ /** Release triggers of a re-key receipt: its moved rows and the uids they left and took. */
995
+ function rekeyTriggers(receipt) {
996
+ const out = [
997
+ { table: receipt.table, uid: receipt.oldUid },
998
+ { table: receipt.table, uid: receipt.newUid },
999
+ {
1000
+ table: receipt.table,
1001
+ held: { entityTable: receipt.table, uid: receipt.newUid, birthFp: receipt.birthFp },
1002
+ },
1003
+ ];
1004
+ for (const child of receipt.cascaded)
1005
+ out.push(...rekeyTriggers(child));
1006
+ return out;
1007
+ }
1008
+ /** The policy each connection receives under; set by {@link receiveRow}'s `policy`. */
1009
+ const receivePolicies = new WeakMap();
1010
+ /**
1011
+ * Resolve the receive policy of a project from its configuration, exactly as
1012
+ * a local task write does (`resolveHierarchyPolicy(loadConfig())`, so
1013
+ * `hierarchy.maxDepth` and `CLEO_HIERARCHY_MAX_DEPTH` apply). The merge engine
1014
+ * resolves it once per merge and passes it to {@link receiveRow}.
1015
+ *
1016
+ * @param cwd - Project root.
1017
+ * @returns The policy.
1018
+ */
1019
+ export async function loadReceivePolicy(cwd) {
1020
+ // Loaded lazily: config.js is outside the store module graph.
1021
+ const { loadConfig } = await import('../config.js');
1022
+ return { maxDepth: resolveHierarchyPolicy(await loadConfig(cwd)).maxDepth };
1023
+ }
1024
+ /** The policy in force on `db`: the last one passed, else the default profile's. */
1025
+ function receivePolicyOf(db) {
1026
+ return (receivePolicies.get(db) ?? {
1027
+ maxDepth: resolveHierarchyPolicy({}).maxDepth,
1028
+ });
1029
+ }
1030
+ /**
1031
+ * Receive one row: place it, or hold it (module docs). The first receive
1032
+ * marks the store's identity values as shared, so the open pass never
1033
+ * clears them (T12746).
1034
+ *
1035
+ * @param db - Connection on the project `cleo.db`, inside the merge transaction.
1036
+ * @param wire - The incoming row.
1037
+ * @param policy - The project's resolved receive policy ({@link loadReceivePolicy}).
1038
+ * It stays in force on this connection, also for held rows released later;
1039
+ * without one, the default hierarchy profile applies.
1040
+ * @returns What happened; a `held` result carries the collision for the caller
1041
+ * (the merge engine) to hand to its authority.
1042
+ */
1043
+ export function receiveRow(db, wire, policy) {
1044
+ if (policy)
1045
+ receivePolicies.set(db, policy);
1046
+ registerRowUidFunction(db, 'project');
1047
+ return inSavepoint(db, 'receive', () => {
1048
+ markRowIdentityShared(db, 'receive');
1049
+ const result = placeRow(db, wire);
1050
+ if (result.status === 'inserted') {
1051
+ releaseHeldRows(db, [{ table: wire.table, uid: result.uid, key: result.key }]);
1052
+ }
1053
+ return result;
1054
+ });
1055
+ }
1056
+ /** Depth of a local task (a root is 0), walking its parent chain. */
1057
+ function depthOf(db, taskId) {
1058
+ const parentOf = db.prepare('SELECT parent_id AS p FROM tasks_tasks WHERE id = ?');
1059
+ let depth = 0;
1060
+ let current = taskId;
1061
+ for (let hop = 0; hop < 64 && current !== null; hop++) {
1062
+ const row = parentOf.get(current);
1063
+ current = row?.p ?? null;
1064
+ if (current !== null)
1065
+ depth++;
1066
+ }
1067
+ return depth;
1068
+ }
1069
+ /**
1070
+ * Whether an insert error is the store refusing the row: SQLite's
1071
+ * SQLITE_CONSTRAINT family (CHECK, NOT NULL, UNIQUE, FOREIGN KEY, and the
1072
+ * guard triggers' `RAISE(ABORT, ...)`, which report SQLITE_CONSTRAINT_TRIGGER).
1073
+ * Anything else (a missing table, an I/O error) is not the row's fault and
1074
+ * is thrown.
1075
+ */
1076
+ function isConstraintError(error) {
1077
+ const code = error?.errcode;
1078
+ return typeof code === 'number' && (code & 0xff) === 19; // SQLITE_CONSTRAINT
1079
+ }
1080
+ /** The claim lease columns: local to the device that took the lease, never received. */
1081
+ const CLAIM_COLUMNS = ['claimed_by_session', 'claimed_by_agent', 'claimed_at', 'lease_expires_at'];
1082
+ /**
1083
+ * Per-table validation of a received row, in place (T12801). A task is checked
1084
+ * against `insertTaskSchema` (the schema every local task write satisfies) and
1085
+ * arrives without a claim lease: a lease belongs to the device and session
1086
+ * that took it. The store's triggers (containment cycle, type matrix, status
1087
+ * pipeline) then run on the insert itself.
1088
+ *
1089
+ * @returns Why the row is invalid, or `null`.
1090
+ */
1091
+ function validateReceived(db, table, values) {
1092
+ if (table !== 'tasks_tasks')
1093
+ return null;
1094
+ // The containment depth cap every local task write enforces, from the
1095
+ // project's resolved hierarchy policy (T12801 review).
1096
+ const parent = values.parent_id;
1097
+ if (typeof parent === 'string' && parent.length > 0) {
1098
+ const { maxDepth } = receivePolicyOf(db);
1099
+ const parentDepth = depthOf(db, parent);
1100
+ if (exceedsMaxDepth(parentDepth, maxDepth)) {
1101
+ return `E_DEPTH_EXCEEDED: depth ${parentDepth + 1} under ${parent} exceeds ${maxDepth}`;
1102
+ }
1103
+ }
1104
+ for (const column of CLAIM_COLUMNS) {
1105
+ if (column in values)
1106
+ values[column] = null;
1107
+ }
1108
+ const byColumn = new Map(Object.entries(getTableColumns(tasksTable)).map(([prop, col]) => [col.name, prop]));
1109
+ const candidate = {};
1110
+ for (const [column, value] of Object.entries(values)) {
1111
+ const prop = byColumn.get(column);
1112
+ if (prop !== undefined && value !== null)
1113
+ candidate[prop] = value;
1114
+ }
1115
+ const parsed = insertTaskSchema.safeParse(candidate);
1116
+ if (parsed.success)
1117
+ return null;
1118
+ const issue = parsed.error.issues[0];
1119
+ return `invalid ${issue?.path.join('.') ?? 'row'}: ${issue?.message ?? 'rejected'}`.slice(0, 200);
1120
+ }
1121
+ /**
1122
+ * Re-try the held rows now (after an op this module did not apply).
1123
+ *
1124
+ * @param db - Connection on the project `cleo.db`.
1125
+ * @returns The rows placed.
1126
+ */
1127
+ export function releaseHeld(db) {
1128
+ registerRowUidFunction(db, 'project');
1129
+ return inSavepoint(db, 'release', () => releaseHeldRows(db, 'all'));
1130
+ }
1131
+ /**
1132
+ * Every held row (read-only: held rows are never edited in place).
1133
+ *
1134
+ * @param db - Connection on the project `cleo.db`.
1135
+ * @returns The held rows, oldest first.
1136
+ */
1137
+ export function listHeldRows(db) {
1138
+ return db
1139
+ .prepare(`SELECT entity_table AS entityTable, uid, birth_fp AS birthFp, reason,
1140
+ contested_id AS contestedId, created_at AS createdAt, row_json AS rowJson
1141
+ FROM ${QUARANTINE_TABLE} ORDER BY created_at, entity_table, uid`)
1142
+ .all().map(({ rowJson, ...rest }) => ({ ...rest, row: JSON.parse(rowJson) }));
1143
+ }
1144
+ /**
1145
+ * The wire form of a local row (what a sender publishes): values without the
1146
+ * identity columns, and every declared reference as the target's uid and
1147
+ * birth fingerprint.
1148
+ *
1149
+ * @param db - Connection on the project `cleo.db`.
1150
+ * @param table - Declared table.
1151
+ * @param uid - The row's uid.
1152
+ * @returns The wire row.
1153
+ */
1154
+ export function wireRowOf(db, table, uid) {
1155
+ const spec = specOf(table);
1156
+ const row = db.prepare(`SELECT * FROM main.${q(table)} WHERE ${q(UID_COLUMN)} = ?`).get(uid);
1157
+ if (!row)
1158
+ throw new Error(`wire: no ${table} row with uid ${uid}`);
1159
+ const keyColumns = localKeyColumns(spec);
1160
+ const values = {};
1161
+ for (const [column, value] of Object.entries(row)) {
1162
+ if (column === UID_COLUMN || column === BIRTH_FP_COLUMN || keyColumns.has(column))
1163
+ continue;
1164
+ values[column] = value;
1165
+ }
1166
+ /** The identity of the target row, by its local key or by its uid. */
1167
+ const identityOf = (target, by, value) => {
1168
+ const minted = specOf(target).kind === 'minted';
1169
+ const t = db
1170
+ .prepare(`SELECT ${q(UID_COLUMN)} AS uid, ${minted ? q(BIRTH_FP_COLUMN) : 'NULL'} AS fp
1171
+ FROM main.${q(target)} WHERE ${q(by === 'key' ? keyOf(target) : UID_COLUMN)} = ?`)
1172
+ .get(value);
1173
+ // A missing target travels as gone (the raw key never travels); one whose
1174
+ // uid is not filled yet travels as uid null (the receiver waits).
1175
+ if (!t)
1176
+ return { uid: null, birthFp: null, gone: true };
1177
+ return t.uid ? { uid: t.uid, birthFp: t.fp } : { uid: null, birthFp: null };
1178
+ };
1179
+ /** The fingerprint a deleted criterion had, from the AC uid graveyard. */
1180
+ const graveyardFp = (acUid) => {
1181
+ const has = db
1182
+ .prepare("SELECT 1 AS x FROM sqlite_master WHERE type = 'table' AND name = ?")
1183
+ .get(AC_UID_GRAVEYARD);
1184
+ if (!has)
1185
+ return null;
1186
+ const r = db
1187
+ .prepare(`SELECT birth_fp AS fp FROM ${AC_UID_GRAVEYARD} WHERE uid = ? ORDER BY seq DESC LIMIT 1`)
1188
+ .get(acUid);
1189
+ return r?.fp ?? null;
1190
+ };
1191
+ const refs = {};
1192
+ for (const [column, target] of refTargets(spec)) {
1193
+ const value = row[column];
1194
+ refs[column] = value === null || value === undefined ? null : identityOf(target, 'key', value);
1195
+ }
1196
+ for (const [column, { table: target, from }] of storedUidRefs(spec)) {
1197
+ const recorded = row[column];
1198
+ const key = row[from];
1199
+ if (recorded !== null && recorded !== undefined) {
1200
+ refs[column] = identityOf(target, 'uid', recorded);
1201
+ // A recorded uid whose row this store no longer has keeps its uid, with
1202
+ // the fingerprint the graveyard kept when there is one; the receiver
1203
+ // never matches it without a fingerprint.
1204
+ if (refs[column]?.uid === null) {
1205
+ refs[column] = {
1206
+ uid: String(recorded),
1207
+ birthFp: graveyardFp(String(recorded)),
1208
+ gone: true,
1209
+ };
1210
+ }
1211
+ }
1212
+ else {
1213
+ refs[column] = key === null || key === undefined ? null : identityOf(target, 'key', key);
1214
+ }
1215
+ }
1216
+ const jsonRefs = {};
1217
+ for (const [column, target] of jsonArrayTargets(spec)) {
1218
+ const raw = row[column];
1219
+ let ids;
1220
+ try {
1221
+ ids = typeof raw === 'string' ? JSON.parse(raw) : null;
1222
+ }
1223
+ catch {
1224
+ ids = null;
1225
+ }
1226
+ if (!Array.isArray(ids)) {
1227
+ values[column] = raw ?? null; // not an id array: sent as is
1228
+ continue;
1229
+ }
1230
+ // An id whose task this store no longer has is dropped from the array:
1231
+ // there is nothing it could name on the receiver (T12798 review).
1232
+ jsonRefs[column] = ids
1233
+ .map((id) => identityOf(target, 'key', String(id)))
1234
+ .filter((ref) => !ref.gone);
1235
+ }
1236
+ return {
1237
+ version: WIRE_VERSION,
1238
+ table,
1239
+ uid,
1240
+ birthFp: spec.kind === 'minted' ? (row[BIRTH_FP_COLUMN] ?? null) : null,
1241
+ values,
1242
+ refs,
1243
+ ...(Object.keys(jsonRefs).length > 0 ? { jsonRefs } : {}),
1244
+ };
1245
+ }
1246
+ /** Stored-uid copies, display aliases, the re-mint record and the uid alias of a moved row. */
1247
+ function followRow(db, table, key, oldUid, birthFp, newUid, options) {
1248
+ const moved = [];
1249
+ if (key !== null) {
1250
+ for (const other of ROW_IDENTITY.project) {
1251
+ for (const ref of other.storedRefUids ?? []) {
1252
+ if (ref.table !== table || (ref.source ?? 'uid') !== 'uid')
1253
+ continue;
1254
+ rewriteStoredRefUidNative(db, other.table, ref.column, ref.from, key, oldUid, newUid);
1255
+ }
1256
+ }
1257
+ }
1258
+ const aliases = db
1259
+ .prepare(`SELECT uid, display_id AS displayId, reason, origin, displaced_hlc AS displacedHlc,
1260
+ created_at AS createdAt
1261
+ FROM ${DISPLAY_ID_ALIAS_TABLE}
1262
+ WHERE entity_table = ? AND entity_uid = ? AND entity_birth_fp = ?`)
1263
+ .all(table, oldUid, birthFp);
1264
+ for (const a of aliases) {
1265
+ deleteDisplayIdAliasNative(db, a.uid);
1266
+ const uid = naturalRowUid('project', DISPLAY_ID_ALIAS_TABLE, [
1267
+ table,
1268
+ a.displayId,
1269
+ newUid,
1270
+ a.reason,
1271
+ ]);
1272
+ insertDisplayIdAliasNative(db, {
1273
+ uid,
1274
+ entityTable: table,
1275
+ displayId: a.displayId,
1276
+ entityUid: newUid,
1277
+ entityBirthFp: birthFp,
1278
+ reason: a.reason,
1279
+ origin: a.origin,
1280
+ displacedHlc: a.displacedHlc,
1281
+ createdAt: a.createdAt,
1282
+ }, aliasPrecedes);
1283
+ moved.push({ table: DISPLAY_ID_ALIAS_TABLE, oldUid: a.uid, newUid: uid });
1284
+ }
1285
+ insertUidAliasNative(db, {
1286
+ uid: naturalRowUid('project', UID_ALIAS_TABLE, [table, oldUid, birthFp]),
1287
+ entityTable: table,
1288
+ oldUid,
1289
+ oldBirthFp: birthFp,
1290
+ newUid,
1291
+ origin: options.origin ?? null,
1292
+ displacedHlc: options.displacedHlc ?? null,
1293
+ createdAt: options.now ?? new Date().toISOString(),
1294
+ });
1295
+ return moved;
1296
+ }
1297
+ /** Natural rows keyed by (`table`, `key`): their table, rowid and uid. */
1298
+ function naturalRowsKeyedBy(db, table, key) {
1299
+ const out = [];
1300
+ for (const other of ROW_IDENTITY.project) {
1301
+ if (other.kind !== 'natural')
1302
+ continue;
1303
+ const keyed = (other.keyRefs ?? []).filter((ref) => ref.table === table);
1304
+ if (keyed.length === 0)
1305
+ continue;
1306
+ const rows = db
1307
+ .prepare(`SELECT rowid AS rowid, ${q(UID_COLUMN)} AS uid FROM main.${q(other.table)}
1308
+ WHERE ${keyed.map((ref) => `${q(ref.column)} = ?`).join(' OR ')}`)
1309
+ .all(...keyed.map(() => key));
1310
+ for (const r of rows)
1311
+ out.push({ table: other.table, ...r });
1312
+ }
1313
+ return out;
1314
+ }
1315
+ /** Minted children of a placed row, found through their owner column. */
1316
+ function localChildren(db, table, key) {
1317
+ const out = [];
1318
+ for (const child of ROW_IDENTITY.project) {
1319
+ if (child.kind !== 'minted')
1320
+ continue;
1321
+ for (const owner of child.owners ?? []) {
1322
+ if (owner.table !== table)
1323
+ continue;
1324
+ const rows = db
1325
+ .prepare(`SELECT ${q(UID_COLUMN)} AS uid, ${q(BIRTH_FP_COLUMN)} AS birthFp
1326
+ FROM main.${q(child.table)}
1327
+ WHERE ${q(owner.column)} = ? AND ${q(UID_COLUMN)} IS NOT NULL
1328
+ AND ${q(BIRTH_FP_COLUMN)} IS NOT NULL
1329
+ ORDER BY ${q(UID_COLUMN)}`)
1330
+ .all(key);
1331
+ for (const r of rows)
1332
+ out.push({ table: child.table, ...r });
1333
+ }
1334
+ }
1335
+ return out;
1336
+ }
1337
+ /** Held minted children of a held row, found through their wire owner ref. */
1338
+ function heldChildren(db, table, uid, birthFp) {
1339
+ const out = [];
1340
+ for (const h of listHeldRows(db)) {
1341
+ const spec = rowIdentitySpec('project', h.entityTable);
1342
+ if (spec?.kind !== 'minted')
1343
+ continue;
1344
+ for (const owner of spec.owners ?? []) {
1345
+ if (owner.table !== table)
1346
+ continue;
1347
+ const ref = h.row.refs?.[owner.column];
1348
+ if (ref && ref.uid === uid && ref.birthFp === birthFp) {
1349
+ out.push({ table: h.entityTable, uid: h.uid, birthFp: h.birthFp });
1350
+ }
1351
+ }
1352
+ }
1353
+ return out;
1354
+ }
1355
+ /** Re-key a held row's wire form. */
1356
+ function rekeyHeldRow(db, key, newUid) {
1357
+ const row = db
1358
+ .prepare(`SELECT row_json AS rowJson FROM ${QUARANTINE_TABLE}
1359
+ WHERE entity_table = ? AND uid = ? AND birth_fp = ?`)
1360
+ .get(key.entityTable, key.uid, key.birthFp);
1361
+ const wire = JSON.parse(row.rowJson);
1362
+ rekeyQuarantineNative(db, key, newUid, JSON.stringify({ ...wire, uid: newUid }));
1363
+ }
1364
+ /** Where a (table, uid, birth fp) row is on this replica. */
1365
+ function locate(db, table, uid, birthFp) {
1366
+ const local = localRow(db, table, uid, birthFp);
1367
+ if (local)
1368
+ return { where: 'local', key: local.key };
1369
+ const held = db
1370
+ .prepare(`SELECT 1 AS x FROM ${QUARANTINE_TABLE} WHERE entity_table = ? AND uid = ? AND birth_fp = ?`)
1371
+ .get(table, uid, birthFp);
1372
+ return held ? { where: 'held' } : { where: 'absent' };
1373
+ }
1374
+ /** AUTHORITY: re-key one row wherever it is, derive its cascade, and return the receipt. */
1375
+ function rekeyDerive(db, table, oldUid, birthFp, newUid, options) {
1376
+ const at = locate(db, table, oldUid, birthFp);
1377
+ const natural = [];
1378
+ let children;
1379
+ if (at.where === 'local') {
1380
+ children = localChildren(db, table, at.key);
1381
+ const keyed = naturalRowsKeyedBy(db, table, at.key);
1382
+ setRowUidNative(db, table, oldUid, birthFp, newUid);
1383
+ natural.push(...followRow(db, table, at.key, oldUid, birthFp, newUid, options));
1384
+ for (const n of keyed)
1385
+ clearRowUidNative(db, n.table, n.rowid);
1386
+ for (const t of new Set(keyed.map((n) => n.table)))
1387
+ fillTableUids(db, 'project', t);
1388
+ for (const n of keyed) {
1389
+ const now = db
1390
+ .prepare(`SELECT ${q(UID_COLUMN)} AS uid FROM main.${q(n.table)} WHERE rowid = ?`)
1391
+ .get(n.rowid);
1392
+ if (n.uid && now.uid && n.uid !== now.uid) {
1393
+ natural.push({ table: n.table, oldUid: n.uid, newUid: now.uid });
1394
+ }
1395
+ }
1396
+ }
1397
+ else if (at.where === 'held') {
1398
+ children = heldChildren(db, table, oldUid, birthFp);
1399
+ rekeyHeldRow(db, { entityTable: table, uid: oldUid, birthFp }, newUid);
1400
+ followRow(db, table, null, oldUid, birthFp, newUid, options);
1401
+ }
1402
+ else {
1403
+ throw new CleoError(ExitCode.NOT_FOUND, `No ${table} row ${oldUid} with birth ${birthFp}`, {
1404
+ details: { field: 'uid', actual: oldUid },
1405
+ });
1406
+ }
1407
+ const cascaded = children.map((c) => rekeyDerive(db, c.table, c.uid, c.birthFp, rekeyedChildUid('project', c.table, c.uid, newUid), options));
1408
+ return {
1409
+ table,
1410
+ oldUid,
1411
+ birthFp,
1412
+ newUid,
1413
+ natural,
1414
+ cascaded,
1415
+ origin: options.origin ?? null,
1416
+ hlc: options.displacedHlc ?? null,
1417
+ };
1418
+ }
1419
+ /**
1420
+ * AUTHORITY ONLY: re-key the LOSER of a uid collision (same uid, different
1421
+ * birth fingerprints; the loser is the greater fingerprint). The caller names
1422
+ * the loser by its fingerprint and the call asserts it: the row re-keyed is
1423
+ * the one with `loserBirthFp`, wherever it is here (placed, or held because
1424
+ * the winner arrived first), and a fingerprint that is not the greater of the
1425
+ * pair is refused. The row gets a random uid; the cascade follows from stored
1426
+ * identity (module docs). Held rows the re-key unblocks are placed.
1427
+ *
1428
+ * @param db - Connection on the project `cleo.db`.
1429
+ * @param table - Minted table of the row.
1430
+ * @param uid - The colliding uid.
1431
+ * @param collision - The loser's and the winner's birth fingerprints (the
1432
+ * winner must be the smaller; T12801).
1433
+ * @param options - The authority and the HLC of the re-key.
1434
+ * @returns The receipt to publish, with its cascade and natural uids.
1435
+ * @throws CleoError when this replica has no row (uid, loserBirthFp), or the
1436
+ * fingerprint is not the loser's.
1437
+ */
1438
+ export function rekeyRowUid(db, table, uid, collision, options = {}) {
1439
+ if (specOf(table).kind !== 'minted') {
1440
+ throw new Error(`rekey: ${table} is not a declared minted table`);
1441
+ }
1442
+ const { loserBirthFp, winnerBirthFp } = collision;
1443
+ // The loser is the greater fingerprint of the pair; the caller names both,
1444
+ // so a swapped or stale pair is refused rather than re-keying the winner.
1445
+ if (!(winnerBirthFp < loserBirthFp)) {
1446
+ throw new CleoError(ExitCode.VALIDATION_ERROR, `rekey: ${loserBirthFp} is not the loser of the ${table} uid collision on ${uid} (the loser is the greater fingerprint; winner ${winnerBirthFp})`, {
1447
+ fix: 'Re-key the row with the greater birth fingerprint; the smaller one keeps the uid.',
1448
+ details: { field: 'loserBirthFp', actual: loserBirthFp, expected: winnerBirthFp },
1449
+ });
1450
+ }
1451
+ const rivals = [
1452
+ ...db
1453
+ .prepare(`SELECT ${q(BIRTH_FP_COLUMN)} AS fp FROM main.${q(table)} WHERE ${q(UID_COLUMN)} = ?`)
1454
+ .all(uid).map((r) => r.fp),
1455
+ ...db
1456
+ .prepare(`SELECT birth_fp AS fp FROM ${QUARANTINE_TABLE} WHERE entity_table = ? AND uid = ?`)
1457
+ .all(table, uid).map((r) => r.fp),
1458
+ ].filter((fp) => fp !== null && fp !== loserBirthFp);
1459
+ const greater = rivals.find((fp) => fp > loserBirthFp || fp !== winnerBirthFp);
1460
+ if (greater !== undefined) {
1461
+ throw new CleoError(ExitCode.VALIDATION_ERROR, `rekey: ${loserBirthFp} is not the loser of the ${table} uid collision on ${uid}`, {
1462
+ fix: 'Re-key the row with the greater birth fingerprint; the smaller one keeps the uid.',
1463
+ details: { field: 'loserBirthFp', actual: loserBirthFp, expected: greater },
1464
+ });
1465
+ }
1466
+ if (locate(db, table, uid, loserBirthFp).where === 'absent') {
1467
+ throw new CleoError(ExitCode.NOT_FOUND, `No ${table} row with uid ${uid} and birth fingerprint ${loserBirthFp} on this replica`, {
1468
+ fix: 'Only a replica that holds the loser re-keys it; the others apply the receipt.',
1469
+ details: { field: 'uid', actual: uid },
1470
+ });
1471
+ }
1472
+ registerRowUidFunction(db, 'project');
1473
+ return inSavepoint(db, 'rekey', () => {
1474
+ const receipt = rekeyDerive(db, table, uid, loserBirthFp, mintRowUid(), options);
1475
+ releaseHeldRows(db, rekeyTriggers(receipt));
1476
+ return receipt;
1477
+ });
1478
+ }
1479
+ /** Apply one receipt entry, then its cascade. */
1480
+ function applyEntry(db, e, out) {
1481
+ const options = { origin: e.origin, displacedHlc: e.hlc };
1482
+ const at = locate(db, e.table, e.oldUid, e.birthFp);
1483
+ let status;
1484
+ if (at.where === 'local') {
1485
+ const keyed = naturalRowsKeyedBy(db, e.table, at.key);
1486
+ setRowUidNative(db, e.table, e.oldUid, e.birthFp, e.newUid);
1487
+ followRow(db, e.table, at.key, e.oldUid, e.birthFp, e.newUid, options);
1488
+ for (const n of e.natural) {
1489
+ if (n.table !== DISPLAY_ID_ALIAS_TABLE)
1490
+ setNaturalUidNative(db, n.table, n.oldUid, n.newUid);
1491
+ }
1492
+ // A natural row the authority did not have (an edge only this replica
1493
+ // holds) is re-derived here; for the published ones this is a no-op.
1494
+ for (const n of keyed)
1495
+ clearRowUidNative(db, n.table, n.rowid);
1496
+ for (const t of new Set(keyed.map((n) => n.table)))
1497
+ fillTableUids(db, 'project', t);
1498
+ status = 'applied';
1499
+ }
1500
+ else if (at.where === 'held') {
1501
+ rekeyHeldRow(db, { entityTable: e.table, uid: e.oldUid, birthFp: e.birthFp }, e.newUid);
1502
+ followRow(db, e.table, null, e.oldUid, e.birthFp, e.newUid, options);
1503
+ status = 'applied-held';
1504
+ }
1505
+ else {
1506
+ status =
1507
+ locate(db, e.table, e.newUid, e.birthFp).where === 'absent'
1508
+ ? 'alias-only'
1509
+ : 'already-applied';
1510
+ followRow(db, e.table, null, e.oldUid, e.birthFp, e.newUid, options);
1511
+ }
1512
+ out.push({ table: e.table, oldUid: e.oldUid, status });
1513
+ for (const child of e.cascaded)
1514
+ applyEntry(db, child, out);
1515
+ }
1516
+ /**
1517
+ * RECEIVER: apply a published {@link RekeyReceipt}. Every entry moves the row
1518
+ * with (old uid, birth fingerprint) to the published new uid, wherever it is
1519
+ * here: a placed row (with its stored copies, display aliases and the
1520
+ * published natural uids), a held row (its wire form), or nowhere (the uid
1521
+ * alias is recorded, so the row takes its new uid when it arrives). A row
1522
+ * with the old uid and ANOTHER fingerprint (the winner) is never touched.
1523
+ * Nothing is recomputed. Held rows the re-key unblocks are placed.
1524
+ *
1525
+ * @param db - Connection on the project `cleo.db`.
1526
+ * @param receipt - The published receipt.
1527
+ * @returns Per-row outcome, and the held rows placed.
1528
+ */
1529
+ export function applyRekey(db, receipt) {
1530
+ registerRowUidFunction(db, 'project');
1531
+ return inSavepoint(db, 'rekey_apply', () => {
1532
+ markRowIdentityShared(db, 'receive');
1533
+ const rows = [];
1534
+ applyEntry(db, receipt, rows);
1535
+ return { rows, released: releaseHeldRows(db, rekeyTriggers(receipt)) };
1536
+ });
1537
+ }
1538
+ //# sourceMappingURL=display-id-alias.js.map