@cleocode/core 2026.9.24 → 2026.9.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/dist/audit-prune.d.ts +32 -0
  2. package/dist/audit-prune.d.ts.map +1 -1
  3. package/dist/audit-prune.js +37 -3
  4. package/dist/audit-prune.js.map +1 -1
  5. package/dist/build-provenance.json +2 -2
  6. package/dist/cloud/crypto.d.ts +1 -2
  7. package/dist/cloud/crypto.d.ts.map +1 -1
  8. package/dist/cloud/crypto.js +1 -11
  9. package/dist/cloud/crypto.js.map +1 -1
  10. package/dist/cloud/uuidv7.d.ts +9 -0
  11. package/dist/cloud/uuidv7.d.ts.map +1 -0
  12. package/dist/cloud/uuidv7.js +19 -0
  13. package/dist/cloud/uuidv7.js.map +1 -0
  14. package/dist/decide/bench/index.d.ts +1 -1
  15. package/dist/decide/bench/index.d.ts.map +1 -1
  16. package/dist/decide/bench/index.js +1 -1
  17. package/dist/decide/bench/index.js.map +1 -1
  18. package/dist/decide/bench/operation.d.ts +1 -1
  19. package/dist/decide/bench/operation.d.ts.map +1 -1
  20. package/dist/decide/bench/operation.js +1 -1
  21. package/dist/decide/bench/operation.js.map +1 -1
  22. package/dist/decide/bench/profiles.d.ts +50 -22
  23. package/dist/decide/bench/profiles.d.ts.map +1 -1
  24. package/dist/decide/bench/profiles.js +103 -59
  25. package/dist/decide/bench/profiles.js.map +1 -1
  26. package/dist/decide/bench/report.d.ts.map +1 -1
  27. package/dist/decide/bench/report.js +6 -1
  28. package/dist/decide/bench/report.js.map +1 -1
  29. package/dist/decide/bench/runner.d.ts +6 -1
  30. package/dist/decide/bench/runner.d.ts.map +1 -1
  31. package/dist/decide/bench/runner.js +1 -0
  32. package/dist/decide/bench/runner.js.map +1 -1
  33. package/dist/decide/client.d.ts.map +1 -1
  34. package/dist/decide/client.js +11 -5
  35. package/dist/decide/client.js.map +1 -1
  36. package/dist/decide/jev-wire.d.ts +31 -1
  37. package/dist/decide/jev-wire.d.ts.map +1 -1
  38. package/dist/decide/jev-wire.js +61 -7
  39. package/dist/decide/jev-wire.js.map +1 -1
  40. package/dist/docs/export-document.js +17181 -15536
  41. package/dist/docs/export-document.js.map +3 -3
  42. package/dist/doctor/orphan-ac-bindings.d.ts +64 -0
  43. package/dist/doctor/orphan-ac-bindings.d.ts.map +1 -0
  44. package/dist/doctor/orphan-ac-bindings.js +59 -0
  45. package/dist/doctor/orphan-ac-bindings.js.map +1 -0
  46. package/dist/doctor/projects.d.ts.map +1 -1
  47. package/dist/doctor/projects.js +43 -29
  48. package/dist/doctor/projects.js.map +1 -1
  49. package/dist/doctor/row-identity.d.ts +31 -0
  50. package/dist/doctor/row-identity.d.ts.map +1 -0
  51. package/dist/doctor/row-identity.js +93 -0
  52. package/dist/doctor/row-identity.js.map +1 -0
  53. package/dist/doctor/twin-collapse.d.ts +58 -2
  54. package/dist/doctor/twin-collapse.d.ts.map +1 -1
  55. package/dist/doctor/twin-collapse.js +212 -14
  56. package/dist/doctor/twin-collapse.js.map +1 -1
  57. package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
  58. package/dist/lifecycle/verification/satisfies-validator.js +10 -0
  59. package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
  60. package/dist/llm/plugin-facade.js +17045 -15400
  61. package/dist/llm/plugin-facade.js.map +3 -3
  62. package/dist/memory/index.d.ts.map +1 -1
  63. package/dist/memory/index.js +19 -10
  64. package/dist/memory/index.js.map +1 -1
  65. package/dist/memory/manifest-identity.d.ts +171 -0
  66. package/dist/memory/manifest-identity.d.ts.map +1 -0
  67. package/dist/memory/manifest-identity.js +345 -0
  68. package/dist/memory/manifest-identity.js.map +1 -0
  69. package/dist/memory/mental-model-queue.d.ts +14 -4
  70. package/dist/memory/mental-model-queue.d.ts.map +1 -1
  71. package/dist/memory/mental-model-queue.js +64 -35
  72. package/dist/memory/mental-model-queue.js.map +1 -1
  73. package/dist/memory/pipeline-manifest-sqlite.d.ts +47 -5
  74. package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
  75. package/dist/memory/pipeline-manifest-sqlite.js +130 -14
  76. package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
  77. package/dist/memory/retrieval/observe.d.ts.map +1 -1
  78. package/dist/memory/retrieval/observe.js +2 -0
  79. package/dist/memory/retrieval/observe.js.map +1 -1
  80. package/dist/memory/transcript-scanner.js +1 -1
  81. package/dist/nexus/deps.d.ts.map +1 -1
  82. package/dist/nexus/deps.js +7 -14
  83. package/dist/nexus/deps.js.map +1 -1
  84. package/dist/nexus/discover.d.ts.map +1 -1
  85. package/dist/nexus/discover.js +6 -9
  86. package/dist/nexus/discover.js.map +1 -1
  87. package/dist/nexus/git-state.d.ts.map +1 -1
  88. package/dist/nexus/git-state.js +10 -1
  89. package/dist/nexus/git-state.js.map +1 -1
  90. package/dist/nexus/projects-clean.d.ts.map +1 -1
  91. package/dist/nexus/projects-clean.js +22 -11
  92. package/dist/nexus/projects-clean.js.map +1 -1
  93. package/dist/nexus/projects-scan.d.ts.map +1 -1
  94. package/dist/nexus/projects-scan.js +8 -13
  95. package/dist/nexus/projects-scan.js.map +1 -1
  96. package/dist/nexus/query.d.ts.map +1 -1
  97. package/dist/nexus/query.js +4 -12
  98. package/dist/nexus/query.js.map +1 -1
  99. package/dist/nexus/registry-roots.d.ts +5 -2
  100. package/dist/nexus/registry-roots.d.ts.map +1 -1
  101. package/dist/nexus/registry-roots.js +8 -10
  102. package/dist/nexus/registry-roots.js.map +1 -1
  103. package/dist/nexus/registry.d.ts +18 -0
  104. package/dist/nexus/registry.d.ts.map +1 -1
  105. package/dist/nexus/registry.js +37 -16
  106. package/dist/nexus/registry.js.map +1 -1
  107. package/dist/orchestrate/worktree-complete.d.ts.map +1 -1
  108. package/dist/orchestrate/worktree-complete.js +40 -2
  109. package/dist/orchestrate/worktree-complete.js.map +1 -1
  110. package/dist/orchestration/spawn-prompt.d.ts +26 -0
  111. package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
  112. package/dist/orchestration/spawn-prompt.js +62 -91
  113. package/dist/orchestration/spawn-prompt.js.map +1 -1
  114. package/dist/release/ci-evidence.d.ts +190 -0
  115. package/dist/release/ci-evidence.d.ts.map +1 -1
  116. package/dist/release/ci-evidence.js +436 -4
  117. package/dist/release/ci-evidence.js.map +1 -1
  118. package/dist/sdk/validator-ac-pull.d.ts.map +1 -1
  119. package/dist/sdk/validator-ac-pull.js +2 -1
  120. package/dist/sdk/validator-ac-pull.js.map +1 -1
  121. package/dist/selfimprove/fix-gen.js +17049 -15404
  122. package/dist/selfimprove/fix-gen.js.map +3 -3
  123. package/dist/sentient/cross-project-hygiene.d.ts +5 -0
  124. package/dist/sentient/cross-project-hygiene.d.ts.map +1 -1
  125. package/dist/sentient/cross-project-hygiene.js +7 -1
  126. package/dist/sentient/cross-project-hygiene.js.map +1 -1
  127. package/dist/sentient/propose-tick.d.ts.map +1 -1
  128. package/dist/sentient/propose-tick.js +2 -3
  129. package/dist/sentient/propose-tick.js.map +1 -1
  130. package/dist/sentient/stage-drift-tick.d.ts.map +1 -1
  131. package/dist/sentient/stage-drift-tick.js +2 -3
  132. package/dist/sentient/stage-drift-tick.js.map +1 -1
  133. package/dist/skills/manifests/research.js +2 -0
  134. package/dist/skills/manifests/research.js.map +1 -1
  135. package/dist/skills/orchestrator/validator.d.ts.map +1 -1
  136. package/dist/skills/orchestrator/validator.js +9 -2
  137. package/dist/skills/orchestrator/validator.js.map +1 -1
  138. package/dist/skills/validation.d.ts +4 -1
  139. package/dist/skills/validation.d.ts.map +1 -1
  140. package/dist/skills/validation.js +11 -3
  141. package/dist/skills/validation.js.map +1 -1
  142. package/dist/spawn/branch-lock.d.ts +111 -7
  143. package/dist/spawn/branch-lock.d.ts.map +1 -1
  144. package/dist/spawn/branch-lock.js +296 -8
  145. package/dist/spawn/branch-lock.js.map +1 -1
  146. package/dist/store/ac-binding-prune.d.ts +144 -0
  147. package/dist/store/ac-binding-prune.d.ts.map +1 -0
  148. package/dist/store/ac-binding-prune.js +293 -0
  149. package/dist/store/ac-binding-prune.js.map +1 -0
  150. package/dist/store/agent-registry-store.d.ts.map +1 -1
  151. package/dist/store/agent-registry-store.js +3 -2
  152. package/dist/store/agent-registry-store.js.map +1 -1
  153. package/dist/store/backup-sidecar.d.ts +85 -2
  154. package/dist/store/backup-sidecar.d.ts.map +1 -1
  155. package/dist/store/backup-sidecar.js +173 -1
  156. package/dist/store/backup-sidecar.js.map +1 -1
  157. package/dist/store/conduit-sqlite.d.ts.map +1 -1
  158. package/dist/store/conduit-sqlite.js +3 -2
  159. package/dist/store/conduit-sqlite.js.map +1 -1
  160. package/dist/store/credential-transfer.d.ts.map +1 -1
  161. package/dist/store/credential-transfer.js +3 -0
  162. package/dist/store/credential-transfer.js.map +1 -1
  163. package/dist/store/db-helpers.d.ts +17 -11
  164. package/dist/store/db-helpers.d.ts.map +1 -1
  165. package/dist/store/db-helpers.js +61 -38
  166. package/dist/store/db-helpers.js.map +1 -1
  167. package/dist/store/display-id-alias.d.ts +529 -0
  168. package/dist/store/display-id-alias.d.ts.map +1 -0
  169. package/dist/store/display-id-alias.js +1063 -0
  170. package/dist/store/display-id-alias.js.map +1 -0
  171. package/dist/store/dual-scope-db.d.ts.map +1 -1
  172. package/dist/store/dual-scope-db.js +21 -0
  173. package/dist/store/dual-scope-db.js.map +1 -1
  174. package/dist/store/exodus/reconcile.d.ts.map +1 -1
  175. package/dist/store/exodus/reconcile.js +7 -2
  176. package/dist/store/exodus/reconcile.js.map +1 -1
  177. package/dist/store/exodus/recovery.d.ts.map +1 -1
  178. package/dist/store/exodus/recovery.js +20 -1
  179. package/dist/store/exodus/recovery.js.map +1 -1
  180. package/dist/store/legacy-tasks-lineage.d.ts.map +1 -1
  181. package/dist/store/legacy-tasks-lineage.js +19 -2
  182. package/dist/store/legacy-tasks-lineage.js.map +1 -1
  183. package/dist/store/row-identity-flag.d.ts +26 -0
  184. package/dist/store/row-identity-flag.d.ts.map +1 -0
  185. package/dist/store/row-identity-flag.js +31 -0
  186. package/dist/store/row-identity-flag.js.map +1 -0
  187. package/dist/store/row-identity-registry.d.ts +61 -0
  188. package/dist/store/row-identity-registry.d.ts.map +1 -0
  189. package/dist/store/row-identity-registry.js +198 -0
  190. package/dist/store/row-identity-registry.js.map +1 -0
  191. package/dist/store/row-identity.d.ts +390 -0
  192. package/dist/store/row-identity.d.ts.map +1 -0
  193. package/dist/store/row-identity.js +1240 -0
  194. package/dist/store/row-identity.js.map +1 -0
  195. package/dist/store/safety-data-accessor.d.ts +2 -0
  196. package/dist/store/safety-data-accessor.d.ts.map +1 -1
  197. package/dist/store/safety-data-accessor.js +4 -0
  198. package/dist/store/safety-data-accessor.js.map +1 -1
  199. package/dist/store/schema/attachments.d.ts +1 -1
  200. package/dist/store/schema/audit.d.ts +4 -4
  201. package/dist/store/schema/background-jobs.d.ts +1 -1
  202. package/dist/store/schema/cleo-global/nexus.d.ts +1 -1
  203. package/dist/store/schema/cleo-global/skills.d.ts +2 -2
  204. package/dist/store/schema/cleo-project/audit.d.ts +4 -4
  205. package/dist/store/schema/cleo-project/docs.d.ts +1 -1
  206. package/dist/store/schema/cleo-project/lifecycle.d.ts +4 -4
  207. package/dist/store/schema/cleo-project/nexus-graph.d.ts +3 -3
  208. package/dist/store/schema/cleo-project/provenance-commits.d.ts +2 -2
  209. package/dist/store/schema/cleo-project/provenance-rest.d.ts +3 -3
  210. package/dist/store/schema/cleo-project/runtime.d.ts +3 -3
  211. package/dist/store/schema/cleo-project/tasks-core-batch2.d.ts +87 -1
  212. package/dist/store/schema/cleo-project/tasks-core-batch2.d.ts.map +1 -1
  213. package/dist/store/schema/cleo-project/tasks-core-batch2.js +35 -0
  214. package/dist/store/schema/cleo-project/tasks-core-batch2.js.map +1 -1
  215. package/dist/store/schema/cleo-project/tasks-core.d.ts +183 -7
  216. package/dist/store/schema/cleo-project/tasks-core.d.ts.map +1 -1
  217. package/dist/store/schema/cleo-project/tasks-core.js +52 -1
  218. package/dist/store/schema/cleo-project/tasks-core.js.map +1 -1
  219. package/dist/store/schema/cleo-shared/brain.d.ts +6 -6
  220. package/dist/store/schema/lifecycle.d.ts +4 -4
  221. package/dist/store/schema/manifest.d.ts +1 -1
  222. package/dist/store/schema/memory-schema.d.ts +5 -5
  223. package/dist/store/schema/nexus-schema.d.ts +3 -3
  224. package/dist/store/schema/provenance/releases.d.ts +1 -1
  225. package/dist/store/schema/skills-schema.d.ts +2 -2
  226. package/dist/store/schema/tasks.d.ts +6 -6
  227. package/dist/store/split-brain-import.d.ts +2 -14
  228. package/dist/store/split-brain-import.d.ts.map +1 -1
  229. package/dist/store/split-brain-import.js +4 -19
  230. package/dist/store/split-brain-import.js.map +1 -1
  231. package/dist/store/sqlite-data-accessor.d.ts +279 -2
  232. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  233. package/dist/store/sqlite-data-accessor.js +544 -44
  234. package/dist/store/sqlite-data-accessor.js.map +1 -1
  235. package/dist/store/sync/clock-store.d.ts +67 -0
  236. package/dist/store/sync/clock-store.d.ts.map +1 -0
  237. package/dist/store/sync/clock-store.js +117 -0
  238. package/dist/store/sync/clock-store.js.map +1 -0
  239. package/dist/store/sync/flags.d.ts +51 -0
  240. package/dist/store/sync/flags.d.ts.map +1 -0
  241. package/dist/store/sync/flags.js +81 -0
  242. package/dist/store/sync/flags.js.map +1 -0
  243. package/dist/store/sync/hlc.d.ts +115 -0
  244. package/dist/store/sync/hlc.d.ts.map +1 -0
  245. package/dist/store/sync/hlc.js +172 -0
  246. package/dist/store/sync/hlc.js.map +1 -0
  247. package/dist/store/sync/index.d.ts +20 -0
  248. package/dist/store/sync/index.d.ts.map +1 -0
  249. package/dist/store/sync/index.js +20 -0
  250. package/dist/store/sync/index.js.map +1 -0
  251. package/dist/store/sync/replica-registry.d.ts +85 -0
  252. package/dist/store/sync/replica-registry.d.ts.map +1 -0
  253. package/dist/store/sync/replica-registry.js +147 -0
  254. package/dist/store/sync/replica-registry.js.map +1 -0
  255. package/dist/store/sync/replica.d.ts +181 -0
  256. package/dist/store/sync/replica.d.ts.map +1 -0
  257. package/dist/store/sync/replica.js +305 -0
  258. package/dist/store/sync/replica.js.map +1 -0
  259. package/dist/store/sync/schema.d.ts +68 -0
  260. package/dist/store/sync/schema.d.ts.map +1 -0
  261. package/dist/store/sync/schema.js +130 -0
  262. package/dist/store/sync/schema.js.map +1 -0
  263. package/dist/store/sync/skew-hold.d.ts +71 -0
  264. package/dist/store/sync/skew-hold.d.ts.map +1 -0
  265. package/dist/store/sync/skew-hold.js +89 -0
  266. package/dist/store/sync/skew-hold.js.map +1 -0
  267. package/dist/store/table-classification.d.ts +10 -8
  268. package/dist/store/table-classification.d.ts.map +1 -1
  269. package/dist/store/table-classification.js +93 -25
  270. package/dist/store/table-classification.js.map +1 -1
  271. package/dist/store/tasks-sqlite.d.ts.map +1 -1
  272. package/dist/store/tasks-sqlite.js +9 -1
  273. package/dist/store/tasks-sqlite.js.map +1 -1
  274. package/dist/store/twin-collapse.d.ts +113 -2
  275. package/dist/store/twin-collapse.d.ts.map +1 -1
  276. package/dist/store/twin-collapse.js +247 -19
  277. package/dist/store/twin-collapse.js.map +1 -1
  278. package/dist/store/umbrella-data-accessor.d.ts +2 -0
  279. package/dist/store/umbrella-data-accessor.d.ts.map +1 -1
  280. package/dist/store/umbrella-data-accessor.js +4 -0
  281. package/dist/store/umbrella-data-accessor.js.map +1 -1
  282. package/dist/store/validation-schemas.d.ts +241 -49
  283. package/dist/store/validation-schemas.d.ts.map +1 -1
  284. package/dist/system/health.d.ts.map +1 -1
  285. package/dist/system/health.js +3 -0
  286. package/dist/system/health.js.map +1 -1
  287. package/dist/system/project-health.d.ts +9 -2
  288. package/dist/system/project-health.d.ts.map +1 -1
  289. package/dist/system/project-health.js +8 -3
  290. package/dist/system/project-health.js.map +1 -1
  291. package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
  292. package/dist/tasks/ac-coverage-gate.js +3 -0
  293. package/dist/tasks/ac-coverage-gate.js.map +1 -1
  294. package/dist/tasks/ac-table.d.ts +35 -7
  295. package/dist/tasks/ac-table.d.ts.map +1 -1
  296. package/dist/tasks/ac-table.js +119 -14
  297. package/dist/tasks/ac-table.js.map +1 -1
  298. package/dist/tasks/add.d.ts.map +1 -1
  299. package/dist/tasks/add.js +48 -2
  300. package/dist/tasks/add.js.map +1 -1
  301. package/dist/tasks/backfill-child-projections.js +47 -6
  302. package/dist/tasks/backfill-child-projections.js.map +1 -1
  303. package/dist/tasks/complete.d.ts.map +1 -1
  304. package/dist/tasks/complete.js +6 -1
  305. package/dist/tasks/complete.js.map +1 -1
  306. package/dist/tasks/completion-evaluation.d.ts.map +1 -1
  307. package/dist/tasks/completion-evaluation.js +3 -0
  308. package/dist/tasks/completion-evaluation.js.map +1 -1
  309. package/dist/tasks/evidence.d.ts.map +1 -1
  310. package/dist/tasks/evidence.js +14 -3
  311. package/dist/tasks/evidence.js.map +1 -1
  312. package/dist/validation/compliance.d.ts +5 -1
  313. package/dist/validation/compliance.d.ts.map +1 -1
  314. package/dist/validation/compliance.js +24 -3
  315. package/dist/validation/compliance.js.map +1 -1
  316. package/dist/validation/index.d.ts +1 -1
  317. package/dist/validation/index.d.ts.map +1 -1
  318. package/dist/validation/index.js +1 -1
  319. package/dist/validation/index.js.map +1 -1
  320. package/dist/validation/protocol-common.d.ts +47 -2
  321. package/dist/validation/protocol-common.d.ts.map +1 -1
  322. package/dist/validation/protocol-common.js +86 -21
  323. package/dist/validation/protocol-common.js.map +1 -1
  324. package/migrations/README.md +12 -0
  325. package/migrations/drizzle-cleo-project/20260928120000_t12341-row-uids/migration.sql +169 -0
  326. package/migrations/sync-journal/20260929140000_t12342-sync-clock/migration.sql +42 -0
  327. package/package.json +13 -13
  328. package/templates/CLEO-REFERENCE.md +4 -0
@@ -16,10 +16,13 @@ import { ARCHIVE_REASON_TOMBSTONE, ExitCode, } from '@cleocode/contracts';
16
16
  import { and, eq, getTableColumns, inArray, isNull, like, lte, ne, notInArray, or, sql, } from 'drizzle-orm';
17
17
  import { CleoError } from '../errors.js';
18
18
  import { captureProjectScope as captureTaskAccessorScope, getProjectRoot, worktreeScope, } from '../project-scope.js';
19
+ import { acTextHash, buildAcRowId } from '../tasks/ac-table.js';
20
+ import { generateAuditLogId, pruneAcBindingsForAcIds, pruneAcBindingsForTask, pruneOrphanAcBindings, selectOrphanAcBindings, } from './ac-binding-prune.js';
19
21
  import { withBackgroundOpCommitBoundary } from './background-ops.js';
20
22
  import { archivedTaskToRow, rowToSession, rowToTask, taskToRow } from './converters.js';
21
23
  import { cleanupBrainRefsOnSessionDelete } from './cross-db-cleanup.js';
22
24
  import { batchUpdateDependencies, insertNewTask, loadDependenciesForTasks, loadRelationsForTasks, parseLabels, updateDependencies, updateTaskLabels, upsertSession, upsertTask, } from './db-helpers.js';
25
+ import { registerRowIdentityWriters } from './row-identity.js';
23
26
  import { tasksAuditLog } from './schema/cleo-project/audit.js';
24
27
  import { resolveCurrentSession } from './session-store.js';
25
28
  import { closeDb, getDb, getNativeTasksDb } from './sqlite.js';
@@ -37,13 +40,67 @@ import { runHeartbeatWrite, withWriteRetry } from './with-retry.js';
37
40
  */
38
41
  let _txSavepointCounter = 0;
39
42
  /**
40
- * Generate a unique audit log entry ID.
41
- * @task T4837
43
+ * Evidence bindings for a set of CURRENT AC ids (T12341).
44
+ *
45
+ * A binding is matched through the criterion's uid (`ac_uid`), which survives
46
+ * an AC edit, and falls back to `ac_id` for a binding whose uid is not filled.
47
+ * A binding recorded against an older text of a criterion is reported under
48
+ * the criterion's current id, so callers keyed by AC id keep working, and is
49
+ * marked `stale` when the text hash it recorded (`ac_text_hash`) differs from
50
+ * the criterion's current text: evidence for different text never satisfies a
51
+ * gate (spec §8.2).
42
52
  */
43
- function generateAuditLogId() {
44
- const epoch = Math.floor(Date.now() / 1000);
45
- const rand = Math.random().toString(36).slice(2, 8);
46
- return `log-${epoch}-${rand}`;
53
+ async function selectAcBindings(db, acIds) {
54
+ if (acIds.length === 0)
55
+ return [];
56
+ const b = schema.evidenceAcBindings;
57
+ const ac = schema.taskAcceptanceCriteria;
58
+ const ids = acIds;
59
+ const columns = {
60
+ id: b.id,
61
+ evidenceAtomId: b.evidenceAtomId,
62
+ acId: b.acId,
63
+ currentAcId: ac.id,
64
+ currentAcText: ac.text,
65
+ recordedTextHash: b.acTextHash,
66
+ bindingType: b.bindingType,
67
+ createdAt: b.createdAt,
68
+ };
69
+ // Two indexed lookups rather than one OR across the join: by the current
70
+ // criterion's uid, then by the recorded ac_id.
71
+ const byUid = await db
72
+ .select(columns)
73
+ .from(b)
74
+ .innerJoin(ac, eq(ac.uid, b.acUid))
75
+ .where(inArray(ac.id, ids))
76
+ .all();
77
+ const byId = await db
78
+ .select(columns)
79
+ .from(b)
80
+ .leftJoin(ac, eq(ac.uid, b.acUid))
81
+ .where(inArray(b.acId, ids))
82
+ .all();
83
+ const rows = [...byUid, ...byId];
84
+ const wanted = new Set(ids);
85
+ const seen = new Set();
86
+ const out = [];
87
+ for (const r of rows) {
88
+ if (seen.has(r.id))
89
+ continue;
90
+ seen.add(r.id);
91
+ const stale = r.recordedTextHash !== null &&
92
+ r.currentAcText !== null &&
93
+ acTextHash(r.currentAcText) !== r.recordedTextHash;
94
+ out.push({
95
+ id: r.id,
96
+ evidenceAtomId: r.evidenceAtomId,
97
+ acId: r.currentAcId && wanted.has(r.currentAcId) ? r.currentAcId : r.acId,
98
+ bindingType: r.bindingType,
99
+ createdAt: r.createdAt,
100
+ ...(stale ? { stale: true } : {}),
101
+ });
102
+ }
103
+ return out;
47
104
  }
48
105
  // ---- Schema meta helpers ----
49
106
  /** Read a JSON blob from the schema_meta table by key. */
@@ -132,6 +189,398 @@ export function advanceTaskIdSequence(nativeDb, floor) {
132
189
  .get();
133
190
  return row?.counter;
134
191
  }
192
+ /**
193
+ * Give the task with `uid` the display id `toId`: a compare-and-set on the
194
+ * stored version AND claim lease that advances the version (so a stale
195
+ * `--if-match` writer fails, #1698), with the claim columns carried unchanged
196
+ * (#1701), then every local reference rewritten. Runs in the caller's
197
+ * transaction with foreign keys deferred to its commit.
198
+ *
199
+ * A collision re-mint is a system write: it passes no claim guard and never
200
+ * takes or drops a lease, but the version bump makes the lease holder's next
201
+ * guarded write re-read. A caller acting for a session (a manual rename)
202
+ * passes `guard`: `expectedUpdatedAt` fails with `E_CONFLICT` when the task
203
+ * moved on, `claim` fails with `E_TASK_CLAIMED` when another session holds it.
204
+ *
205
+ * @param nativeDb - The project `cleo.db` handle holding the transaction.
206
+ * @param uid - Uid of the task to rename.
207
+ * @param toId - Its new display id.
208
+ * @param refs - Columns that hold task display ids.
209
+ * @param guard - Optional version / claim guard.
210
+ * @returns The id it had and the rows rewritten per `table.column`.
211
+ * @task T12341
212
+ * @task T12748
213
+ */
214
+ export function renameTaskDisplayIdNative(nativeDb, uid, toId, refs, guard = {}) {
215
+ const current = nativeDb
216
+ .prepare(`SELECT id, updated_at AS updatedAt, created_at AS createdAt,
217
+ claimed_by_session AS claimedBySession, claimed_by_agent AS claimedByAgent,
218
+ claimed_at AS claimedAt, lease_expires_at AS leaseExpiresAt
219
+ FROM tasks_tasks WHERE uid = ?`)
220
+ .get(uid);
221
+ if (!current)
222
+ throw new Error(`No task with uid ${uid}`);
223
+ if (guard.expectedUpdatedAt !== undefined && guard.expectedUpdatedAt !== taskVersion(current)) {
224
+ throw taskConflictError(current.id, guard.expectedUpdatedAt, current);
225
+ }
226
+ if (guard.claim)
227
+ assertClaimAllowed(current.id, current, guard.claim, []);
228
+ nativeDb.exec('PRAGMA defer_foreign_keys = ON');
229
+ const changed = nativeDb
230
+ .prepare(`UPDATE tasks_tasks SET id = ?, updated_at = ?
231
+ WHERE uid = ? AND id = ? AND updated_at IS ? AND claimed_by_session IS ?
232
+ AND lease_expires_at IS ?`)
233
+ .run(toId, nextTaskVersion(current), uid, current.id, current.updatedAt, current.claimedBySession, current.leaseExpiresAt).changes;
234
+ if (Number(changed) !== 1) {
235
+ throw taskConflictError(current.id, taskVersion(current), null);
236
+ }
237
+ const rewritten = {};
238
+ const q = (name) => `"${name.replaceAll('"', '""')}"`;
239
+ for (const ref of refs) {
240
+ const table = `main.${q(ref.table)}`;
241
+ const col = q(ref.column);
242
+ const n = ref.jsonArray
243
+ ? nativeDb
244
+ .prepare(`UPDATE ${table} SET ${col} = (
245
+ SELECT json_group_array(CASE WHEN j.value = ? THEN ? ELSE j.value END)
246
+ FROM json_each(${table}.${col}) j)
247
+ WHERE json_valid(${col}) AND EXISTS (
248
+ SELECT 1 FROM json_each(${table}.${col}) j WHERE j.value = ?)`)
249
+ .run(current.id, toId, current.id).changes
250
+ : nativeDb.prepare(`UPDATE ${table} SET ${col} = ? WHERE ${col} = ?`).run(toId, current.id)
251
+ .changes;
252
+ if (Number(n) > 0)
253
+ rewritten[`${ref.table}.${ref.column}`] = Number(n);
254
+ }
255
+ for (const [key, n] of Object.entries(rederiveAcIdsNative(nativeDb, current.id, toId))) {
256
+ if (n > 0)
257
+ rewritten[key] = n;
258
+ }
259
+ return { fromId: current.id, rewritten };
260
+ }
261
+ /**
262
+ * After a task's display id changed from `fromId` to `toId`, re-derive the
263
+ * ids of the acceptance criteria derived from it (`buildAcRowId(taskId,
264
+ * identity)`): the task's own criteria, and a parent's child-projection
265
+ * criteria (`child:<id>`, whose source key is rewritten too). The binding and
266
+ * history rows that point at a re-derived id follow. A criterion whose id was
267
+ * not derived from the old id (an explicit id) keeps it. Without this, two
268
+ * tasks that once shared a display id keep identical ids for same-text
269
+ * criteria ("tests pass") and collide on every merge (T12799).
270
+ *
271
+ * @param nativeDb - The project `cleo.db` handle, inside the rename's transaction.
272
+ * @param fromId - The task's old display id.
273
+ * @param toId - Its new display id (the task row already carries it).
274
+ * @returns Rows rewritten per `table.column`.
275
+ * @task T12799
276
+ */
277
+ export function rederiveAcIdsNative(nativeDb, fromId, toId) {
278
+ const count = {};
279
+ const bump = (key, n) => {
280
+ count[key] = (count[key] ?? 0) + Number(n);
281
+ };
282
+ const has = (table) => nativeDb
283
+ .prepare("SELECT 1 AS x FROM sqlite_master WHERE type = 'table' AND name = ?")
284
+ .get(table) !== undefined;
285
+ if (!has('tasks_task_acceptance_criteria'))
286
+ return count;
287
+ const oldChild = `child:${fromId}`;
288
+ const rows = nativeDb
289
+ .prepare(`SELECT id, task_id AS taskId, kind, text, source_key AS sourceKey
290
+ FROM tasks_task_acceptance_criteria WHERE task_id = ? OR source_key = ?`)
291
+ .all(toId, oldChild);
292
+ const followers = ['tasks_evidence_ac_bindings', 'tasks_task_acceptance_criteria_history'].filter(has);
293
+ for (const row of rows) {
294
+ const ownerBefore = row.taskId === toId ? fromId : row.taskId;
295
+ const sourceAfter = row.sourceKey === oldChild ? `child:${toId}` : row.sourceKey;
296
+ const identity = (source) => (row.kind === 'text' ? row.text : (source ?? ''));
297
+ const derived = row.id === buildAcRowId(ownerBefore, identity(row.sourceKey));
298
+ const newId = derived ? buildAcRowId(row.taskId, identity(sourceAfter)) : row.id;
299
+ if (newId === row.id && sourceAfter === row.sourceKey)
300
+ continue;
301
+ bump('tasks_task_acceptance_criteria.id', nativeDb
302
+ .prepare('UPDATE tasks_task_acceptance_criteria SET id = ?, source_key = ? WHERE id = ?')
303
+ .run(newId, sourceAfter, row.id).changes);
304
+ if (newId === row.id)
305
+ continue;
306
+ for (const table of followers) {
307
+ bump(`${table}.ac_id`, nativeDb.prepare(`UPDATE ${table} SET ac_id = ? WHERE ac_id = ?`).run(newId, row.id)
308
+ .changes);
309
+ }
310
+ }
311
+ return count;
312
+ }
313
+ /**
314
+ * Insert a display-id alias row (idempotent on its uid).
315
+ *
316
+ * @param nativeDb - The project `cleo.db` handle.
317
+ * @param row - The alias.
318
+ * @task T12341
319
+ */
320
+ export function insertDisplayIdAliasNative(nativeDb, row) {
321
+ nativeDb
322
+ .prepare(`INSERT OR IGNORE INTO tasks_display_id_aliases
323
+ (uid, entity_table, display_id, entity_uid, reason, origin, displaced_hlc, created_at,
324
+ entity_birth_fp)
325
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
326
+ .run(row.uid, row.entityTable, row.displayId, row.entityUid, row.reason, row.origin, row.displacedHlc, row.createdAt, row.entityBirthFp);
327
+ }
328
+ /**
329
+ * Delete one display-id alias row (a local alias whose entity was re-keyed is
330
+ * re-inserted under its new entity uid by the caller).
331
+ *
332
+ * @param nativeDb - The project `cleo.db` handle.
333
+ * @param uid - The alias row's uid.
334
+ * @task T12341
335
+ */
336
+ export function deleteDisplayIdAliasNative(nativeDb, uid) {
337
+ nativeDb.prepare('DELETE FROM tasks_display_id_aliases WHERE uid = ?').run(uid);
338
+ }
339
+ /**
340
+ * Insert a uid re-key alias row (idempotent on its uid).
341
+ *
342
+ * @param nativeDb - The project `cleo.db` handle.
343
+ * @param row - The alias.
344
+ * @task T12341
345
+ */
346
+ export function insertUidAliasNative(nativeDb, row) {
347
+ nativeDb
348
+ .prepare(`INSERT OR IGNORE INTO tasks_uid_aliases
349
+ (uid, entity_table, old_uid, old_birth_fp, new_uid, origin, displaced_hlc, created_at)
350
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
351
+ .run(row.uid, row.entityTable, row.oldUid, row.oldBirthFp, row.newUid, row.origin, row.displacedHlc, row.createdAt);
352
+ }
353
+ /**
354
+ * Hold an incoming row the merge cannot place yet (replacing an older hold of
355
+ * the same row).
356
+ *
357
+ * @param nativeDb - The project `cleo.db` handle.
358
+ * @param row - The held row.
359
+ * @task T12341
360
+ */
361
+ export function insertQuarantineNative(nativeDb, row) {
362
+ nativeDb
363
+ .prepare(`INSERT INTO tasks_identity_quarantine
364
+ (entity_table, uid, birth_fp, reason, contested_id, row_json, received_hlc, created_at)
365
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
366
+ ON CONFLICT(entity_table, uid, birth_fp) DO UPDATE SET
367
+ reason = excluded.reason, contested_id = excluded.contested_id,
368
+ row_json = excluded.row_json, received_hlc = excluded.received_hlc,
369
+ created_at = excluded.created_at`)
370
+ .run(row.entityTable, row.uid, row.birthFp, row.reason, row.contestedId, row.rowJson, row.receivedHlc, row.createdAt);
371
+ }
372
+ /**
373
+ * Drop a held row (released or superseded).
374
+ *
375
+ * @param nativeDb - The project `cleo.db` handle.
376
+ * @param key - The held row's table, uid and birth fingerprint.
377
+ * @task T12341
378
+ */
379
+ export function deleteQuarantineNative(nativeDb, key) {
380
+ nativeDb
381
+ .prepare('DELETE FROM tasks_identity_quarantine WHERE entity_table = ? AND uid = ? AND birth_fp = ?')
382
+ .run(key.entityTable, key.uid, key.birthFp);
383
+ }
384
+ /**
385
+ * Write a row-identity meta value (the recipe marker, a re-mint record).
386
+ *
387
+ * @param nativeDb - The project `cleo.db` handle.
388
+ * @param key - Meta key.
389
+ * @param value - Meta value.
390
+ * @task T12341
391
+ */
392
+ export function writeRowIdentityMetaNative(nativeDb, key, value) {
393
+ nativeDb
394
+ .prepare(`INSERT INTO tasks_row_identity_meta (key, value) VALUES (?, ?)
395
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
396
+ .run(key, value);
397
+ }
398
+ /**
399
+ * Hand a criterion back the uid its deleted predecessor carried (AC uid
400
+ * graveyard re-link, spec §6.5), with that predecessor's birth fingerprint.
401
+ *
402
+ * @param nativeDb - The project `cleo.db` handle.
403
+ * @param acId - The criterion to re-link (its uid is NULL).
404
+ * @param uid - The uid to hand back.
405
+ * @param birthFp - The fingerprint that goes with it, when recorded.
406
+ * @returns Rows changed (0 or 1).
407
+ * @task T12341
408
+ */
409
+ export function relinkAcUidNative(nativeDb, acId, uid, birthFp) {
410
+ return Number(nativeDb
411
+ .prepare('UPDATE tasks_task_acceptance_criteria SET uid = ?, birth_fp = COALESCE(?, birth_fp) WHERE id = ? AND uid IS NULL')
412
+ .run(uid, birthFp, acId).changes);
413
+ }
414
+ /**
415
+ * Empty the AC uid graveyard (after the open pass consumed it).
416
+ *
417
+ * @param nativeDb - The project `cleo.db` handle.
418
+ * @task T12341
419
+ */
420
+ export function clearAcUidGraveyardNative(nativeDb) {
421
+ nativeDb.exec('DELETE FROM tasks_ac_uid_graveyard');
422
+ }
423
+ /** Quote an identifier for the dynamic row-identity writes below. */
424
+ function quoteIdent(name) {
425
+ return `"${name.replaceAll('"', '""')}"`;
426
+ }
427
+ /**
428
+ * Fill a NULL identity column (`uid`, `birth_fp`, a stored reference uid) of a
429
+ * declared table from its recipe SQL (the row-uid open pass, spec §6.1).
430
+ *
431
+ * @param nativeDb - The `cleo.db` handle holding the cold-open lease.
432
+ * @param table - Declared table.
433
+ * @param column - Identity column.
434
+ * @param valueSql - Recipe expression over `main."<table>"` columns.
435
+ * @param rowid - One row only (the clash fallback); all NULL rows otherwise.
436
+ * @returns Rows written.
437
+ * @task T12341
438
+ */
439
+ export function fillIdentityColumnNative(nativeDb, table, column, valueSql, rowid) {
440
+ const col = quoteIdent(column);
441
+ const sql = `UPDATE main.${quoteIdent(table)} SET ${col} = ${valueSql} WHERE ${col} IS NULL`;
442
+ const changes = rowid === undefined
443
+ ? nativeDb.prepare(sql).run().changes
444
+ : nativeDb.prepare(`${sql} AND rowid = ?`).run(rowid).changes;
445
+ return Number(changes);
446
+ }
447
+ /**
448
+ * Clear one pre-release birth fingerprint so the fill re-derives it (spec §12.1).
449
+ *
450
+ * @param nativeDb - The `cleo.db` handle holding the cold-open lease.
451
+ * @param table - Declared minted table.
452
+ * @param rowid - The row.
453
+ * @task T12341
454
+ */
455
+ export function clearBirthFpNative(nativeDb, table, rowid) {
456
+ nativeDb
457
+ .prepare(`UPDATE main.${quoteIdent(table)} SET birth_fp = NULL WHERE rowid = ?`)
458
+ .run(rowid);
459
+ }
460
+ /**
461
+ * Insert one row into a declared identity table, with the columns the row
462
+ * carries that the table has (a row released from the identity quarantine).
463
+ *
464
+ * @param nativeDb - The project `cleo.db` handle.
465
+ * @param table - Declared table.
466
+ * @param row - Column → value.
467
+ * @task T12341
468
+ */
469
+ export function insertIdentityRowNative(nativeDb, table, row) {
470
+ const have = new Set(nativeDb.prepare('SELECT name FROM pragma_table_info(?)').all(table).map((c) => c.name));
471
+ const cols = Object.keys(row).filter((c) => have.has(c));
472
+ nativeDb
473
+ .prepare(`INSERT INTO main.${quoteIdent(table)} (${cols.map(quoteIdent).join(', ')}) VALUES (${cols
474
+ .map(() => '?')
475
+ .join(', ')})`)
476
+ .run(...cols.map((c) => row[c] ?? null));
477
+ }
478
+ /**
479
+ * Give the row of `table` identified by (`oldUid`, `birthFp`) the uid
480
+ * `newUid` (a uid re-key; the birth fingerprint never changes).
481
+ *
482
+ * @param nativeDb - The project `cleo.db` handle.
483
+ * @param table - Declared minted table.
484
+ * @param oldUid - The colliding uid.
485
+ * @param birthFp - The row's birth fingerprint.
486
+ * @param newUid - Its new uid.
487
+ * @returns Rows changed (0 or 1).
488
+ * @task T12341
489
+ */
490
+ export function setRowUidNative(nativeDb, table, oldUid, birthFp, newUid) {
491
+ return Number(nativeDb
492
+ .prepare(`UPDATE main.${quoteIdent(table)} SET uid = ? WHERE uid = ? AND birth_fp = ?`)
493
+ .run(newUid, oldUid, birthFp).changes);
494
+ }
495
+ /**
496
+ * Point a stored reference uid column (`ac_uid`) of the rows that reference
497
+ * the re-keyed row by key at its new uid. Scoped by the key column, so a row
498
+ * referencing the OTHER holder of a colliding uid is never touched.
499
+ *
500
+ * @param nativeDb - The project `cleo.db` handle.
501
+ * @param table - Table holding the column.
502
+ * @param column - The stored reference uid column.
503
+ * @param fromColumn - The column that references the row by key (`ac_id`).
504
+ * @param key - The re-keyed row's local key.
505
+ * @param oldUid - Old referenced uid.
506
+ * @param newUid - New referenced uid.
507
+ * @returns Rows changed.
508
+ * @task T12341
509
+ */
510
+ export function rewriteStoredRefUidNative(nativeDb, table, column, fromColumn, key, oldUid, newUid) {
511
+ const col = quoteIdent(column);
512
+ return Number(nativeDb
513
+ .prepare(`UPDATE main.${quoteIdent(table)} SET ${col} = ? WHERE ${col} = ? AND ${quoteIdent(fromColumn)} = ?`)
514
+ .run(newUid, oldUid, key).changes);
515
+ }
516
+ /**
517
+ * Clear one row's uid so the natural recipe re-derives it (a uid re-key
518
+ * changed an endpoint's uid).
519
+ *
520
+ * @param nativeDb - The project `cleo.db` handle.
521
+ * @param table - Declared natural table.
522
+ * @param rowid - The row.
523
+ * @task T12341
524
+ */
525
+ export function clearRowUidNative(nativeDb, table, rowid) {
526
+ nativeDb.prepare(`UPDATE main.${quoteIdent(table)} SET uid = NULL WHERE rowid = ?`).run(rowid);
527
+ }
528
+ /**
529
+ * Delete a row-identity meta value (a re-mint record that moved with a re-key).
530
+ *
531
+ * @param nativeDb - The project `cleo.db` handle.
532
+ * @param key - Meta key.
533
+ * @task T12341
534
+ */
535
+ export function deleteRowIdentityMetaNative(nativeDb, key) {
536
+ nativeDb.prepare('DELETE FROM tasks_row_identity_meta WHERE key = ?').run(key);
537
+ }
538
+ /**
539
+ * Set a natural row's uid by its current uid (a re-key re-derives the natural
540
+ * uids keyed by the re-keyed row; a receiver applies the published values).
541
+ *
542
+ * @param nativeDb - The project `cleo.db` handle.
543
+ * @param table - Declared natural table.
544
+ * @param oldUid - Current uid.
545
+ * @param newUid - New uid.
546
+ * @returns Rows changed.
547
+ * @task T12341
548
+ */
549
+ export function setNaturalUidNative(nativeDb, table, oldUid, newUid) {
550
+ return Number(nativeDb
551
+ .prepare(`UPDATE main.${quoteIdent(table)} SET uid = ? WHERE uid = ?`)
552
+ .run(newUid, oldUid).changes);
553
+ }
554
+ /**
555
+ * Rewrite a held row (identity quarantine) under a new uid: a uid re-key of a
556
+ * row this replica holds but has not placed.
557
+ *
558
+ * @param nativeDb - The project `cleo.db` handle.
559
+ * @param key - The held row's table, uid and birth fingerprint.
560
+ * @param newUid - Its new uid.
561
+ * @param rowJson - Its wire form with the new uid.
562
+ * @task T12341
563
+ */
564
+ export function rekeyQuarantineNative(nativeDb, key, newUid, rowJson) {
565
+ nativeDb
566
+ .prepare(`UPDATE tasks_identity_quarantine SET uid = ?, row_json = ?
567
+ WHERE entity_table = ? AND uid = ? AND birth_fp = ?`)
568
+ .run(newUid, rowJson, key.entityTable, key.uid, key.birthFp);
569
+ }
570
+ /**
571
+ * Add labels to a task's label junction (idempotent), for writers that
572
+ * insert a task with raw SQL (the sentient proposal ticks).
573
+ *
574
+ * @param nativeDb - The project `cleo.db` handle.
575
+ * @param taskId - The task.
576
+ * @param labels - Labels to add.
577
+ * @task T12341
578
+ */
579
+ export function addTaskLabelsNative(nativeDb, taskId, labels) {
580
+ const stmt = nativeDb.prepare('INSERT OR IGNORE INTO tasks_task_labels (task_id, label) VALUES (?, ?)');
581
+ for (const label of labels)
582
+ stmt.run(taskId, label);
583
+ }
135
584
  // One queue per shared native handle, independent of accessor identity. These
136
585
  // hold coordination state only; the ProjectStore remains the owner of handles.
137
586
  const taskTransactionQueue = new WeakMap();
@@ -607,6 +1056,8 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
607
1056
  createdAt: schema.taskAcceptanceCriteria.createdAt,
608
1057
  updatedAt: schema.taskAcceptanceCriteria.updatedAt,
609
1058
  contentHash: schema.taskAcceptanceCriteria.contentHash,
1059
+ uid: schema.taskAcceptanceCriteria.uid,
1060
+ birthFp: schema.taskAcceptanceCriteria.birthFp,
610
1061
  })
611
1062
  .from(schema.taskAcceptanceCriteria)
612
1063
  .where(eq(schema.taskAcceptanceCriteria.taskId, taskId))
@@ -624,31 +1075,20 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
624
1075
  createdAt: r.createdAt,
625
1076
  updatedAt: r.updatedAt ?? null,
626
1077
  contentHash: r.contentHash ?? null,
1078
+ uid: r.uid ?? null,
1079
+ birthFp: r.birthFp ?? null,
627
1080
  }));
628
1081
  },
629
1082
  // ---- AC bindings (T10509 — AC-coverage gate) ----
630
1083
  async getAcBindings(acIds) {
631
1084
  if (acIds.length === 0)
632
1085
  return [];
1086
+ return selectAcBindings(await getDb(cwd), acIds);
1087
+ },
1088
+ // ---- AC bindings — orphan scan (T12790) ----
1089
+ async findOrphanAcBindings() {
633
1090
  const db = await getDb(cwd);
634
- const rows = await db
635
- .select({
636
- id: schema.evidenceAcBindings.id,
637
- evidenceAtomId: schema.evidenceAcBindings.evidenceAtomId,
638
- acId: schema.evidenceAcBindings.acId,
639
- bindingType: schema.evidenceAcBindings.bindingType,
640
- createdAt: schema.evidenceAcBindings.createdAt,
641
- })
642
- .from(schema.evidenceAcBindings)
643
- .where(inArray(schema.evidenceAcBindings.acId, acIds))
644
- .all();
645
- return rows.map((r) => ({
646
- id: r.id,
647
- evidenceAtomId: r.evidenceAtomId,
648
- acId: r.acId,
649
- bindingType: r.bindingType,
650
- createdAt: r.createdAt,
651
- }));
1091
+ return selectOrphanAcBindings(db);
652
1092
  },
653
1093
  async archiveSingleTask(taskId, fields) {
654
1094
  return accessor.transaction(async () => {
@@ -692,6 +1132,9 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
692
1132
  .delete(schema.taskDependencies)
693
1133
  .where(eq(schema.taskDependencies.dependsOn, taskId))
694
1134
  .run();
1135
+ // T12790: the bindings table has no FK to the AC rows that cascade
1136
+ // from tasks_tasks — remove (and audit) them explicitly.
1137
+ await pruneAcBindingsForTask(db, taskId);
695
1138
  // Delete the task itself
696
1139
  await db.delete(schema.tasks).where(eq(schema.tasks.id, taskId)).run();
697
1140
  });
@@ -1242,6 +1685,8 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1242
1685
  .delete(schema.taskDependencies)
1243
1686
  .where(eq(schema.taskDependencies.dependsOn, taskId))
1244
1687
  .run();
1688
+ // T12790: bindings of the cascading AC rows (no FK).
1689
+ await pruneAcBindingsForTask(db, taskId);
1245
1690
  await db.delete(schema.tasks).where(eq(schema.tasks.id, taskId)).run();
1246
1691
  });
1247
1692
  },
@@ -1321,6 +1766,9 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1321
1766
  projection: r.projection ?? 'legacy',
1322
1767
  text: r.text,
1323
1768
  contentHash: r.contentHash ?? null,
1769
+ // A kept criterion keeps its uid (T12341); else one is minted.
1770
+ ...(r.uid ? { uid: r.uid } : {}),
1771
+ ...(r.uid && r.birthFp ? { birthFp: r.birthFp } : {}),
1324
1772
  })))
1325
1773
  .run();
1326
1774
  });
@@ -1340,6 +1788,8 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1340
1788
  createdAt: schema.taskAcceptanceCriteria.createdAt,
1341
1789
  updatedAt: schema.taskAcceptanceCriteria.updatedAt,
1342
1790
  contentHash: schema.taskAcceptanceCriteria.contentHash,
1791
+ uid: schema.taskAcceptanceCriteria.uid,
1792
+ birthFp: schema.taskAcceptanceCriteria.birthFp,
1343
1793
  })
1344
1794
  .from(schema.taskAcceptanceCriteria)
1345
1795
  .where(eq(schema.taskAcceptanceCriteria.taskId, taskId))
@@ -1357,18 +1807,70 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1357
1807
  createdAt: r.createdAt,
1358
1808
  updatedAt: r.updatedAt ?? null,
1359
1809
  contentHash: r.contentHash ?? null,
1810
+ uid: r.uid ?? null,
1811
+ birthFp: r.birthFp ?? null,
1360
1812
  }));
1361
1813
  },
1362
1814
  async deleteAcRowsForTask(taskId) {
1363
1815
  scope.assertActive();
1364
1816
  return accessor.transaction(async () => {
1365
1817
  scope.assertActive();
1818
+ // T12790: remove (and audit) the departing rows' bindings.
1819
+ await pruneAcBindingsForTask(db, taskId, 'ac-removed');
1366
1820
  await db
1367
1821
  .delete(schema.taskAcceptanceCriteria)
1368
1822
  .where(eq(schema.taskAcceptanceCriteria.taskId, taskId))
1369
1823
  .run();
1370
1824
  });
1371
1825
  },
1826
+ async deleteAcRowsByIds(taskId, ids, keepBindingsForUids = []) {
1827
+ scope.assertActive();
1828
+ if (ids.length === 0)
1829
+ return;
1830
+ return accessor.transaction(async () => {
1831
+ scope.assertActive();
1832
+ // T12790: only ids this task actually owns leave the set —
1833
+ // prune (and audit) exactly their bindings.
1834
+ const owned = await db
1835
+ .select({ id: schema.taskAcceptanceCriteria.id })
1836
+ .from(schema.taskAcceptanceCriteria)
1837
+ .where(and(eq(schema.taskAcceptanceCriteria.taskId, taskId), inArray(schema.taskAcceptanceCriteria.id, ids)))
1838
+ .all();
1839
+ await pruneAcBindingsForAcIds(db, taskId, owned.map((r) => r.id), 'ac-removed', keepBindingsForUids);
1840
+ await db
1841
+ .delete(schema.taskAcceptanceCriteria)
1842
+ .where(and(eq(schema.taskAcceptanceCriteria.taskId, taskId), inArray(schema.taskAcceptanceCriteria.id, ids)))
1843
+ .run();
1844
+ });
1845
+ },
1846
+ async updateAcRows(rows) {
1847
+ scope.assertActive();
1848
+ if (rows.length === 0)
1849
+ return;
1850
+ return accessor.transaction(async () => {
1851
+ scope.assertActive();
1852
+ const updatedAt = new Date().toISOString();
1853
+ for (const r of rows) {
1854
+ const result = await db
1855
+ .update(schema.taskAcceptanceCriteria)
1856
+ .set({
1857
+ ordinal: r.ordinal,
1858
+ kind: r.kind ?? 'text',
1859
+ sourceKey: r.sourceKey ?? `text:${r.ordinal}`,
1860
+ targetTaskId: r.targetTaskId ?? null,
1861
+ projection: r.projection ?? 'legacy',
1862
+ text: r.text,
1863
+ contentHash: r.contentHash ?? null,
1864
+ updatedAt,
1865
+ })
1866
+ .where(and(eq(schema.taskAcceptanceCriteria.id, r.id), eq(schema.taskAcceptanceCriteria.taskId, r.taskId)))
1867
+ .run();
1868
+ if (Number(result.changes) === 0) {
1869
+ throw new CleoError(ExitCode.VALIDATION_ERROR, `Acceptance criterion ${r.id} does not exist on task ${r.taskId}; refusing to update it`);
1870
+ }
1871
+ }
1872
+ });
1873
+ },
1372
1874
  async appendAcHistory(rows) {
1373
1875
  scope.assertActive();
1374
1876
  return accessor.transaction(async () => {
@@ -1381,6 +1883,7 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1381
1883
  acId: r.acId,
1382
1884
  previousText: r.previousText,
1383
1885
  reason: r.reason,
1886
+ ...(r.acUid ? { acUid: r.acUid } : {}),
1384
1887
  })))
1385
1888
  .run();
1386
1889
  });
@@ -1388,26 +1891,7 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1388
1891
  // ---- AC bindings (T10509 — AC-coverage gate) ----
1389
1892
  async getAcBindings(acIds) {
1390
1893
  scope.assertActive();
1391
- if (acIds.length === 0)
1392
- return [];
1393
- const out = await db
1394
- .select({
1395
- id: schema.evidenceAcBindings.id,
1396
- evidenceAtomId: schema.evidenceAcBindings.evidenceAtomId,
1397
- acId: schema.evidenceAcBindings.acId,
1398
- bindingType: schema.evidenceAcBindings.bindingType,
1399
- createdAt: schema.evidenceAcBindings.createdAt,
1400
- })
1401
- .from(schema.evidenceAcBindings)
1402
- .where(inArray(schema.evidenceAcBindings.acId, acIds))
1403
- .all();
1404
- return out.map((r) => ({
1405
- id: r.id,
1406
- evidenceAtomId: r.evidenceAtomId,
1407
- acId: r.acId,
1408
- bindingType: r.bindingType,
1409
- createdAt: r.createdAt,
1410
- }));
1894
+ return selectAcBindings(db, acIds);
1411
1895
  },
1412
1896
  // ---- AC bindings — writer (T10511, Validator SDK tools) ----
1413
1897
  async insertAcBindings(rows) {
@@ -1436,6 +1920,14 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1436
1920
  .run();
1437
1921
  });
1438
1922
  },
1923
+ // ---- AC bindings — orphan repair (T12790) ----
1924
+ async pruneOrphanAcBindings() {
1925
+ scope.assertActive();
1926
+ return accessor.transaction(async () => {
1927
+ scope.assertActive();
1928
+ return pruneOrphanAcBindings(db);
1929
+ });
1930
+ },
1439
1931
  };
1440
1932
  const result = await fn(bindTaskAccessorScope(tx, accessorScope));
1441
1933
  await scope.pending;
@@ -1624,4 +2116,12 @@ async function createOwnedSqliteDataAccessor(cwd, accessorScope) {
1624
2116
  };
1625
2117
  return accessor;
1626
2118
  }
2119
+ // T12341: the row-identity open pass writes through these chokepoint helpers.
2120
+ registerRowIdentityWriters({
2121
+ relinkAcUidNative,
2122
+ clearAcUidGraveyardNative,
2123
+ writeRowIdentityMetaNative,
2124
+ fillIdentityColumnNative,
2125
+ clearBirthFpNative,
2126
+ });
1627
2127
  //# sourceMappingURL=sqlite-data-accessor.js.map