@cavelang/cli 0.35.0 → 0.36.1

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 (327) hide show
  1. package/README.md +224 -12
  2. package/dist/internal/act/action.js +2 -1
  3. package/dist/internal/act/action.js.map +1 -1
  4. package/dist/internal/act/declare.d.ts.map +1 -1
  5. package/dist/internal/act/declare.js +82 -53
  6. package/dist/internal/act/declare.js.map +1 -1
  7. package/dist/internal/act/engine.d.ts +4 -3
  8. package/dist/internal/act/engine.d.ts.map +1 -1
  9. package/dist/internal/act/engine.js +121 -32
  10. package/dist/internal/act/engine.js.map +1 -1
  11. package/dist/internal/automate/cancellation.d.ts +3 -0
  12. package/dist/internal/automate/cancellation.d.ts.map +1 -0
  13. package/dist/internal/automate/cancellation.js +38 -0
  14. package/dist/internal/automate/cancellation.js.map +1 -0
  15. package/dist/internal/automate/declare.d.ts.map +1 -1
  16. package/dist/internal/automate/declare.js +49 -24
  17. package/dist/internal/automate/declare.js.map +1 -1
  18. package/dist/internal/automate/engine.d.ts +7 -3
  19. package/dist/internal/automate/engine.d.ts.map +1 -1
  20. package/dist/internal/automate/engine.js +196 -93
  21. package/dist/internal/automate/engine.js.map +1 -1
  22. package/dist/internal/automate/error-message.d.ts +3 -0
  23. package/dist/internal/automate/error-message.d.ts.map +1 -0
  24. package/dist/internal/automate/error-message.js +10 -0
  25. package/dist/internal/automate/error-message.js.map +1 -0
  26. package/dist/internal/automate/main.d.ts.map +1 -1
  27. package/dist/internal/automate/main.js +238 -35
  28. package/dist/internal/automate/main.js.map +1 -1
  29. package/dist/internal/automate/options.d.ts +4 -0
  30. package/dist/internal/automate/options.d.ts.map +1 -0
  31. package/dist/internal/automate/options.js +19 -0
  32. package/dist/internal/automate/options.js.map +1 -0
  33. package/dist/internal/connect/declared.d.ts +6 -27
  34. package/dist/internal/connect/declared.d.ts.map +1 -1
  35. package/dist/internal/connect/declared.js +165 -76
  36. package/dist/internal/connect/declared.js.map +1 -1
  37. package/dist/internal/connect/error-message.d.ts +3 -0
  38. package/dist/internal/connect/error-message.d.ts.map +1 -0
  39. package/dist/internal/connect/error-message.js +10 -0
  40. package/dist/internal/connect/error-message.js.map +1 -0
  41. package/dist/internal/connect/federation.d.ts +9 -0
  42. package/dist/internal/connect/federation.d.ts.map +1 -0
  43. package/dist/internal/connect/federation.js +19 -0
  44. package/dist/internal/connect/federation.js.map +1 -0
  45. package/dist/internal/connect/fetch-failure.d.ts +3 -0
  46. package/dist/internal/connect/fetch-failure.d.ts.map +1 -0
  47. package/dist/internal/connect/fetch-failure.js +36 -0
  48. package/dist/internal/connect/fetch-failure.js.map +1 -0
  49. package/dist/internal/connect/main.d.ts +1 -0
  50. package/dist/internal/connect/main.d.ts.map +1 -1
  51. package/dist/internal/connect/main.js +288 -114
  52. package/dist/internal/connect/main.js.map +1 -1
  53. package/dist/internal/connect/options.d.ts +3 -0
  54. package/dist/internal/connect/options.d.ts.map +1 -0
  55. package/dist/internal/connect/options.js +9 -0
  56. package/dist/internal/connect/options.js.map +1 -0
  57. package/dist/internal/connect/records.d.ts +3 -0
  58. package/dist/internal/connect/records.d.ts.map +1 -0
  59. package/dist/internal/connect/records.js +21 -0
  60. package/dist/internal/connect/records.js.map +1 -0
  61. package/dist/internal/connect/run.d.ts +1 -18
  62. package/dist/internal/connect/run.d.ts.map +1 -1
  63. package/dist/internal/connect/run.js +115 -55
  64. package/dist/internal/connect/run.js.map +1 -1
  65. package/dist/internal/connect/source.d.ts +3 -1
  66. package/dist/internal/connect/source.d.ts.map +1 -1
  67. package/dist/internal/connect/source.js +226 -47
  68. package/dist/internal/connect/source.js.map +1 -1
  69. package/dist/internal/connect/template.d.ts +2 -2
  70. package/dist/internal/connect/template.d.ts.map +1 -1
  71. package/dist/internal/connect/template.js +25 -9
  72. package/dist/internal/connect/template.js.map +1 -1
  73. package/dist/internal/connect/utf8.d.ts +3 -0
  74. package/dist/internal/connect/utf8.d.ts.map +1 -0
  75. package/dist/internal/connect/utf8.js +12 -0
  76. package/dist/internal/connect/utf8.js.map +1 -0
  77. package/dist/internal/eval/cleanup.d.ts +5 -0
  78. package/dist/internal/eval/cleanup.d.ts.map +1 -0
  79. package/dist/internal/eval/cleanup.js +50 -0
  80. package/dist/internal/eval/cleanup.js.map +1 -0
  81. package/dist/internal/eval/error-message.d.ts +3 -0
  82. package/dist/internal/eval/error-message.d.ts.map +1 -0
  83. package/dist/internal/eval/error-message.js +10 -0
  84. package/dist/internal/eval/error-message.js.map +1 -0
  85. package/dist/internal/eval/judge.d.ts.map +1 -1
  86. package/dist/internal/eval/judge.js +3 -31
  87. package/dist/internal/eval/judge.js.map +1 -1
  88. package/dist/internal/eval/loop.d.ts.map +1 -1
  89. package/dist/internal/eval/loop.js +18 -4
  90. package/dist/internal/eval/loop.js.map +1 -1
  91. package/dist/internal/eval/main.d.ts.map +1 -1
  92. package/dist/internal/eval/main.js +22 -10
  93. package/dist/internal/eval/main.js.map +1 -1
  94. package/dist/internal/eval/options.d.ts +6 -0
  95. package/dist/internal/eval/options.d.ts.map +1 -0
  96. package/dist/internal/eval/options.js +31 -0
  97. package/dist/internal/eval/options.js.map +1 -0
  98. package/dist/internal/eval/queries.d.ts.map +1 -1
  99. package/dist/internal/eval/queries.js +9 -6
  100. package/dist/internal/eval/queries.js.map +1 -1
  101. package/dist/internal/eval/run.d.ts.map +1 -1
  102. package/dist/internal/eval/run.js +112 -24
  103. package/dist/internal/eval/run.js.map +1 -1
  104. package/dist/internal/eval/score.d.ts.map +1 -1
  105. package/dist/internal/eval/score.js +9 -2
  106. package/dist/internal/eval/score.js.map +1 -1
  107. package/dist/internal/eval/suite.d.ts.map +1 -1
  108. package/dist/internal/eval/suite.js +14 -2
  109. package/dist/internal/eval/suite.js.map +1 -1
  110. package/dist/internal/ingest/cleanup.d.ts +5 -0
  111. package/dist/internal/ingest/cleanup.d.ts.map +1 -0
  112. package/dist/internal/ingest/cleanup.js +50 -0
  113. package/dist/internal/ingest/cleanup.js.map +1 -0
  114. package/dist/internal/ingest/content.d.ts +6 -0
  115. package/dist/internal/ingest/content.d.ts.map +1 -0
  116. package/dist/internal/ingest/content.js +25 -0
  117. package/dist/internal/ingest/content.js.map +1 -0
  118. package/dist/internal/ingest/context.d.ts.map +1 -1
  119. package/dist/internal/ingest/context.js +25 -7
  120. package/dist/internal/ingest/context.js.map +1 -1
  121. package/dist/internal/ingest/error-message.d.ts +3 -0
  122. package/dist/internal/ingest/error-message.d.ts.map +1 -0
  123. package/dist/internal/ingest/error-message.js +10 -0
  124. package/dist/internal/ingest/error-message.js.map +1 -0
  125. package/dist/internal/ingest/files.d.ts +4 -12
  126. package/dist/internal/ingest/files.d.ts.map +1 -1
  127. package/dist/internal/ingest/files.js +22 -10
  128. package/dist/internal/ingest/files.js.map +1 -1
  129. package/dist/internal/ingest/main.d.ts.map +1 -1
  130. package/dist/internal/ingest/main.js +46 -16
  131. package/dist/internal/ingest/main.js.map +1 -1
  132. package/dist/internal/ingest/options.d.ts +6 -0
  133. package/dist/internal/ingest/options.d.ts.map +1 -0
  134. package/dist/internal/ingest/options.js +27 -0
  135. package/dist/internal/ingest/options.js.map +1 -0
  136. package/dist/internal/ingest/prompt.d.ts.map +1 -1
  137. package/dist/internal/ingest/prompt.js +14 -10
  138. package/dist/internal/ingest/prompt.js.map +1 -1
  139. package/dist/internal/ingest/run.d.ts +9 -2
  140. package/dist/internal/ingest/run.d.ts.map +1 -1
  141. package/dist/internal/ingest/run.js +206 -62
  142. package/dist/internal/ingest/run.js.map +1 -1
  143. package/dist/internal/ingest/timeout.d.ts +3 -0
  144. package/dist/internal/ingest/timeout.d.ts.map +1 -0
  145. package/dist/internal/ingest/timeout.js +14 -0
  146. package/dist/internal/ingest/timeout.js.map +1 -0
  147. package/dist/internal/ingest/web.d.ts +2 -11
  148. package/dist/internal/ingest/web.d.ts.map +1 -1
  149. package/dist/internal/ingest/web.js +131 -25
  150. package/dist/internal/ingest/web.js.map +1 -1
  151. package/dist/internal/loop/budgets.d.ts +3 -0
  152. package/dist/internal/loop/budgets.d.ts.map +1 -0
  153. package/dist/internal/loop/budgets.js +10 -0
  154. package/dist/internal/loop/budgets.js.map +1 -0
  155. package/dist/internal/loop/index.d.ts +2 -0
  156. package/dist/internal/loop/index.d.ts.map +1 -1
  157. package/dist/internal/loop/index.js +2 -0
  158. package/dist/internal/loop/index.js.map +1 -1
  159. package/dist/internal/loop/json-answer.d.ts +7 -0
  160. package/dist/internal/loop/json-answer.d.ts.map +1 -0
  161. package/dist/internal/loop/json-answer.js +79 -0
  162. package/dist/internal/loop/json-answer.js.map +1 -0
  163. package/dist/internal/loop/json-spans.d.ts +3 -0
  164. package/dist/internal/loop/json-spans.d.ts.map +1 -0
  165. package/dist/internal/loop/json-spans.js +88 -0
  166. package/dist/internal/loop/json-spans.js.map +1 -0
  167. package/dist/internal/loop/llm.d.ts +3 -2
  168. package/dist/internal/loop/llm.d.ts.map +1 -1
  169. package/dist/internal/loop/llm.js +115 -32
  170. package/dist/internal/loop/llm.js.map +1 -1
  171. package/dist/internal/loop/process.d.ts +3 -1
  172. package/dist/internal/loop/process.d.ts.map +1 -1
  173. package/dist/internal/loop/process.js +159 -59
  174. package/dist/internal/loop/process.js.map +1 -1
  175. package/dist/internal/loop/read-snapshot.d.ts +4 -0
  176. package/dist/internal/loop/read-snapshot.d.ts.map +1 -0
  177. package/dist/internal/loop/read-snapshot.js +29 -0
  178. package/dist/internal/loop/read-snapshot.js.map +1 -0
  179. package/dist/internal/loop/reconstruct.d.ts +6 -5
  180. package/dist/internal/loop/reconstruct.d.ts.map +1 -1
  181. package/dist/internal/loop/reconstruct.js +19 -8
  182. package/dist/internal/loop/reconstruct.js.map +1 -1
  183. package/dist/internal/loop/score-settings.d.ts +5 -0
  184. package/dist/internal/loop/score-settings.d.ts.map +1 -0
  185. package/dist/internal/loop/score-settings.js +14 -0
  186. package/dist/internal/loop/score-settings.js.map +1 -0
  187. package/dist/internal/loop/sqlite.d.ts.map +1 -1
  188. package/dist/internal/loop/sqlite.js +15 -7
  189. package/dist/internal/loop/sqlite.js.map +1 -1
  190. package/dist/internal/loop/store.d.ts.map +1 -1
  191. package/dist/internal/loop/store.js +11 -7
  192. package/dist/internal/loop/store.js.map +1 -1
  193. package/dist/internal/loop/timeout.d.ts +3 -0
  194. package/dist/internal/loop/timeout.d.ts.map +1 -0
  195. package/dist/internal/loop/timeout.js +13 -0
  196. package/dist/internal/loop/timeout.js.map +1 -0
  197. package/dist/internal/mcp/error-message.d.ts +3 -0
  198. package/dist/internal/mcp/error-message.d.ts.map +1 -0
  199. package/dist/internal/mcp/error-message.js +10 -0
  200. package/dist/internal/mcp/error-message.js.map +1 -0
  201. package/dist/internal/mcp/main.d.ts.map +1 -1
  202. package/dist/internal/mcp/main.js +42 -27
  203. package/dist/internal/mcp/main.js.map +1 -1
  204. package/dist/internal/mcp/read-snapshot.d.ts +4 -0
  205. package/dist/internal/mcp/read-snapshot.d.ts.map +1 -0
  206. package/dist/internal/mcp/read-snapshot.js +22 -0
  207. package/dist/internal/mcp/read-snapshot.js.map +1 -0
  208. package/dist/internal/mcp/server.d.ts.map +1 -1
  209. package/dist/internal/mcp/server.js +181 -21
  210. package/dist/internal/mcp/server.js.map +1 -1
  211. package/dist/internal/mcp/source.d.ts +3 -0
  212. package/dist/internal/mcp/source.d.ts.map +1 -0
  213. package/dist/internal/mcp/source.js +13 -0
  214. package/dist/internal/mcp/source.js.map +1 -0
  215. package/dist/internal/mcp/tools.d.ts.map +1 -1
  216. package/dist/internal/mcp/tools.js +135 -72
  217. package/dist/internal/mcp/tools.js.map +1 -1
  218. package/dist/internal/rules/declarations.d.ts +6 -0
  219. package/dist/internal/rules/declarations.d.ts.map +1 -0
  220. package/dist/internal/rules/declarations.js +14 -0
  221. package/dist/internal/rules/declarations.js.map +1 -0
  222. package/dist/internal/rules/declare.d.ts.map +1 -1
  223. package/dist/internal/rules/declare.js +30 -16
  224. package/dist/internal/rules/declare.js.map +1 -1
  225. package/dist/internal/rules/engine.d.ts +8 -7
  226. package/dist/internal/rules/engine.d.ts.map +1 -1
  227. package/dist/internal/rules/engine.js +150 -44
  228. package/dist/internal/rules/engine.js.map +1 -1
  229. package/dist/internal/shape/alias-root.d.ts +3 -0
  230. package/dist/internal/shape/alias-root.d.ts.map +1 -0
  231. package/dist/internal/shape/alias-root.js +13 -0
  232. package/dist/internal/shape/alias-root.js.map +1 -0
  233. package/dist/internal/shape/check.d.ts +15 -3
  234. package/dist/internal/shape/check.d.ts.map +1 -1
  235. package/dist/internal/shape/check.js +230 -100
  236. package/dist/internal/shape/check.js.map +1 -1
  237. package/dist/internal/shape/client.d.ts.map +1 -1
  238. package/dist/internal/shape/client.js +42 -20
  239. package/dist/internal/shape/client.js.map +1 -1
  240. package/dist/internal/shape/constraints.d.ts +8 -0
  241. package/dist/internal/shape/constraints.d.ts.map +1 -0
  242. package/dist/internal/shape/constraints.js +17 -0
  243. package/dist/internal/shape/constraints.js.map +1 -0
  244. package/dist/internal/shape/edit-distance.d.ts +3 -0
  245. package/dist/internal/shape/edit-distance.d.ts.map +1 -0
  246. package/dist/internal/shape/edit-distance.js +33 -0
  247. package/dist/internal/shape/edit-distance.js.map +1 -0
  248. package/dist/internal/shape/gate.d.ts.map +1 -1
  249. package/dist/internal/shape/gate.js +0 -0
  250. package/dist/internal/shape/gate.js.map +1 -1
  251. package/dist/internal/shape/snapshot.d.ts +4 -0
  252. package/dist/internal/shape/snapshot.d.ts.map +1 -0
  253. package/dist/internal/shape/snapshot.js +20 -0
  254. package/dist/internal/shape/snapshot.js.map +1 -0
  255. package/dist/internal/shape/suggest.d.ts +8 -6
  256. package/dist/internal/shape/suggest.d.ts.map +1 -1
  257. package/dist/internal/shape/suggest.js +208 -92
  258. package/dist/internal/shape/suggest.js.map +1 -1
  259. package/dist/internal/sync/error-message.d.ts +3 -0
  260. package/dist/internal/sync/error-message.d.ts.map +1 -0
  261. package/dist/internal/sync/error-message.js +10 -0
  262. package/dist/internal/sync/error-message.js.map +1 -0
  263. package/dist/internal/sync/index.d.ts +3 -2
  264. package/dist/internal/sync/index.d.ts.map +1 -1
  265. package/dist/internal/sync/index.js +3 -2
  266. package/dist/internal/sync/index.js.map +1 -1
  267. package/dist/internal/sync/sync.d.ts +8 -5
  268. package/dist/internal/sync/sync.d.ts.map +1 -1
  269. package/dist/internal/sync/sync.js +322 -65
  270. package/dist/internal/sync/sync.js.map +1 -1
  271. package/dist/internal/view/api.d.ts +3 -1
  272. package/dist/internal/view/api.d.ts.map +1 -1
  273. package/dist/internal/view/api.js +154 -90
  274. package/dist/internal/view/api.js.map +1 -1
  275. package/dist/internal/view/error-message.d.ts +3 -0
  276. package/dist/internal/view/error-message.d.ts.map +1 -0
  277. package/dist/internal/view/error-message.js +10 -0
  278. package/dist/internal/view/error-message.js.map +1 -0
  279. package/dist/internal/view/main.d.ts.map +1 -1
  280. package/dist/internal/view/main.js +61 -8
  281. package/dist/internal/view/main.js.map +1 -1
  282. package/dist/internal/view/page.d.ts.map +1 -1
  283. package/dist/internal/view/page.js +119 -20
  284. package/dist/internal/view/page.js.map +1 -1
  285. package/dist/internal/view/report.d.ts.map +1 -1
  286. package/dist/internal/view/report.js +180 -145
  287. package/dist/internal/view/report.js.map +1 -1
  288. package/dist/internal/view/scope.d.ts +6 -2
  289. package/dist/internal/view/scope.d.ts.map +1 -1
  290. package/dist/internal/view/scope.js +107 -24
  291. package/dist/internal/view/scope.js.map +1 -1
  292. package/dist/internal/view/sensitivity.d.ts +4 -0
  293. package/dist/internal/view/sensitivity.d.ts.map +1 -0
  294. package/dist/internal/view/sensitivity.js +11 -0
  295. package/dist/internal/view/sensitivity.js.map +1 -0
  296. package/dist/internal/view/server.d.ts.map +1 -1
  297. package/dist/internal/view/server.js +57 -20
  298. package/dist/internal/view/server.js.map +1 -1
  299. package/dist/src/atomic-output.d.ts +3 -0
  300. package/dist/src/atomic-output.d.ts.map +1 -0
  301. package/dist/src/atomic-output.js +67 -0
  302. package/dist/src/atomic-output.js.map +1 -0
  303. package/dist/src/cli.d.ts +3 -2
  304. package/dist/src/cli.d.ts.map +1 -1
  305. package/dist/src/cli.js +188 -173
  306. package/dist/src/cli.js.map +1 -1
  307. package/dist/src/dispatch.d.ts.map +1 -1
  308. package/dist/src/dispatch.js +3 -2
  309. package/dist/src/dispatch.js.map +1 -1
  310. package/dist/src/doctor.d.ts.map +1 -1
  311. package/dist/src/doctor.js +178 -102
  312. package/dist/src/doctor.js.map +1 -1
  313. package/dist/src/error-message.d.ts +3 -0
  314. package/dist/src/error-message.d.ts.map +1 -0
  315. package/dist/src/error-message.js +20 -0
  316. package/dist/src/error-message.js.map +1 -0
  317. package/dist/src/hooks-config.d.ts +3 -0
  318. package/dist/src/hooks-config.d.ts.map +1 -0
  319. package/dist/src/hooks-config.js +23 -0
  320. package/dist/src/hooks-config.js.map +1 -0
  321. package/package.json +23 -20
  322. package/src/atomic-output.ts +47 -0
  323. package/src/cli.ts +170 -147
  324. package/src/dispatch.ts +3 -2
  325. package/src/doctor.ts +192 -111
  326. package/src/error-message.ts +13 -0
  327. package/src/hooks-config.ts +22 -0
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * A row's UUIDv7 — one value serving as both `id` and `tx` — is its global
5
5
  * identity (§28.1): merging copies rows absent by id verbatim (side tables
6
- * included) and skips rows the target already has, which makes sync
7
- * idempotent, transitive and bidirectional, and can never conflict —
6
+ * included) and skips matching rows the target already has. A reused id
7
+ * with different content is rejected. Valid replicas are idempotent,
8
+ * transitive and bidirectional —
8
9
  * coexisting contradictions are legal data (§9.4), resolved at read time
9
10
  * (§26). Merged transaction ids feed the generator's receive rule (§28.2),
10
11
  * so everything appended after a merge sorts after everything merged,
@@ -34,6 +35,7 @@ export type SyncOptions = {
34
35
  readonly dryRun?: boolean;
35
36
  };
36
37
  export type SyncProblem = {
38
+ /** Source text line, or zero for a database-source problem. */
37
39
  readonly line: number;
38
40
  readonly message: string;
39
41
  };
@@ -47,12 +49,12 @@ export type SyncReport = {
47
49
  readonly dryRun: boolean;
48
50
  /** The §28.3 merge record appended, when the merge was effective and records are on. */
49
51
  readonly record?: string;
50
- /** Text-source problems (spec §28.4 strictness); nothing merges when non-empty. */
52
+ /** Source identity or text-validation problems; nothing merges when non-empty. */
51
53
  readonly problems: readonly SyncProblem[];
52
54
  };
53
55
  /**
54
56
  * @returns text usable as one entity token in a §28.3 record subject:
55
- * whitespace and comment/metadata sigils normalized to `-`, never empty.
57
+ * whitespace, literal delimiters, attribute colons and comment/metadata sigils normalized to `-`, never empty.
56
58
  */
57
59
  export declare const sanitizeLabel: (text: string) => string;
58
60
  /**
@@ -66,7 +68,8 @@ export declare const labelOf: (path: string) => string;
66
68
  * tags, FTS — edges dedupe against stored edges, merged in-band
67
69
  * declarations reload the registry, and the generator observes the merged
68
70
  * transaction ids (§28.2). Idempotent: a re-run merges nothing and appends
69
- * no record.
71
+ * no record. Conflicting existing identities reject the whole source before
72
+ * copying, with database problems reported at line zero.
70
73
  */
71
74
  export declare const syncDb: (store: Store, sourcePath: string, options?: SyncOptions) => SyncReport;
72
75
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,OAAO,EAAsB,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAE7E,OAAO,EAAE,WAAW,EAAE,CAAA;AAEtB,MAAM,MAAM,WAAW,GAAG;IACxB,oHAAoH;IACpH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,gEAAgE;IAChE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,sDAAsD;IACtD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,wFAAwF;IACxF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAA;CAC1C,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MAG5C,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,MAGtC,CAAA;AA4CD;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,GAAI,OAAO,KAAK,EAAE,YAAY,MAAM,EAAE,UAAS,WAAgB,KAAG,UAyIpF,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,KAAK,EAAE,MAAM,MAAM,EAAE,UAAS,WAAgB,KAAG,UAsEhF,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,KAAK,EAAE,YAAY,MAAM,EAAE,UAAS,WAAgB,KAAG,UAKtF,CAAA"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,OAAO,EAA2B,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAElF,OAAO,EAAE,WAAW,EAAE,CAAA;AAEtB,MAAM,MAAM,WAAW,GAAG;IACxB,oHAAoH;IACpH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,gEAAgE;IAChE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,sDAAsD;IACtD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,wFAAwF;IACxF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAA;CAC1C,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MAG5C,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,MAGtC,CAAA;AA+DD;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM,GAAI,OAAO,KAAK,EAAE,YAAY,MAAM,EAAE,UAAS,WAAgB,KAAG,UAmRpF,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,KAAK,EAAE,MAAM,MAAM,EAAE,UAAS,WAAgB,KAAG,UAOhF,CAAA;AAsID;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,KAAK,EAAE,YAAY,MAAM,EAAE,UAAS,WAAgB,KAAG,UAgBtF,CAAA"}
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * A row's UUIDv7 — one value serving as both `id` and `tx` — is its global
5
5
  * identity (§28.1): merging copies rows absent by id verbatim (side tables
6
- * included) and skips rows the target already has, which makes sync
7
- * idempotent, transitive and bidirectional, and can never conflict —
6
+ * included) and skips matching rows the target already has. A reused id
7
+ * with different content is rejected. Valid replicas are idempotent,
8
+ * transitive and bidirectional —
8
9
  * coexisting contradictions are legal data (§9.4), resolved at read time
9
10
  * (§26). Merged transaction ids feed the generator's receive rule (§28.2),
10
11
  * so everything appended after a merge sorts after everything merged,
@@ -21,17 +22,18 @@
21
22
  * `store/<from> SYNCED-INTO store/<into> @src:sync` — whose belief series
22
23
  * is the sync log; a merge that changed nothing appends nothing.
23
24
  */
24
- import { existsSync, readFileSync, realpathSync } from 'node:fs';
25
- import { Uuidv7 } from '@cavelang/core';
25
+ import { errorMessage } from "./error-message.js";
26
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
27
+ import { Claim, Key, Uuidv7 } from '@cavelang/core';
26
28
  import * as Canonical from '@cavelang/canonical';
27
- import { Provenance, Schema, isStoreFile } from '@cavelang/store';
29
+ import { Provenance, Row, Schema, isStoreFile } from '@cavelang/store';
28
30
  export { isStoreFile };
29
31
  /**
30
32
  * @returns text usable as one entity token in a §28.3 record subject:
31
- * whitespace and comment/metadata sigils normalized to `-`, never empty.
33
+ * whitespace, literal delimiters, attribute colons and comment/metadata sigils normalized to `-`, never empty.
32
34
  */
33
35
  export const sanitizeLabel = (text) => {
34
- const label = text.replaceAll(/[\s;@#]+/g, '-');
36
+ const label = text.replaceAll(/[\s;:@#"`]+/g, '-');
35
37
  return label === '' ? 'store' : label;
36
38
  };
37
39
  /**
@@ -65,7 +67,7 @@ const rolledBack = (store, body) => {
65
67
  * §9.5 path.
66
68
  */
67
69
  const appendRecord = (store, from, into, merged, edges) => {
68
- const declared = store.db.prepare("SELECT 1 FROM cave_claim WHERE subject = 'SYNCED-INTO' AND verb = 'IS' AND object = 'verb' AND negated = 0 LIMIT 1").get() !== undefined;
70
+ const declared = Canonical.Registry.isDeclared(store.registry(), 'SYNCED-INTO');
69
71
  const line = `store/${sanitizeLabel(from)} SYNCED-INTO store/${sanitizeLabel(into)} ; +${merged} claim(s), +${edges} edge(s)`;
70
72
  const text = declared ?
71
73
  line :
@@ -80,40 +82,85 @@ const claimColumns = [
80
82
  'delta_text', 'delta_num', 'delta_unit', 'sigma_level',
81
83
  'conf', 'importance', 'comment', 'raw_line', 'claim_key'
82
84
  ].join(', ');
85
+ /** Preserve operation failures when releasing a sync-owned SQL resource also fails. */
86
+ const withSyncCleanup = (run, cleanup) => {
87
+ let result;
88
+ try {
89
+ result = run();
90
+ }
91
+ catch (error) {
92
+ try {
93
+ cleanup();
94
+ }
95
+ catch (cleanupError) {
96
+ const messages = [error, cleanupError].map(errorMessage);
97
+ throw new AggregateError([error, cleanupError], `database sync failed: ${messages[0]}; cleanup also failed: ${messages[1]}`, { cause: error });
98
+ }
99
+ throw error;
100
+ }
101
+ cleanup();
102
+ return result;
103
+ };
104
+ const booleanOption = (value, name, fallback) => {
105
+ if (value === undefined)
106
+ return fallback;
107
+ if (typeof value !== 'boolean')
108
+ throw new TypeError(`${name} must be a boolean`);
109
+ return value;
110
+ };
83
111
  /**
84
112
  * Merges another CAVE store file into `store` (spec §28.1): rows absent by
85
113
  * id are copied verbatim — `id`, `tx`, `claim_key`, `raw_line`, contexts,
86
114
  * tags, FTS — edges dedupe against stored edges, merged in-band
87
115
  * declarations reload the registry, and the generator observes the merged
88
116
  * transaction ids (§28.2). Idempotent: a re-run merges nothing and appends
89
- * no record.
117
+ * no record. Conflicting existing identities reject the whole source before
118
+ * copying, with database problems reported at line zero.
90
119
  */
91
120
  export const syncDb = (store, sourcePath, options = {}) => {
121
+ const dryRun = booleanOption(options.dryRun, 'dryRun', false);
122
+ const recordEnabled = booleanOption(options.record, 'record', true);
92
123
  if (!existsSync(sourcePath)) {
93
124
  // ATTACH would create an empty database at the missing path.
94
125
  throw new Error(`${sourcePath}: no such file`);
95
126
  }
96
127
  const db = store.db;
97
- const dryRun = options.dryRun === true;
128
+ const from = options.from ?? 'origin';
129
+ const into = options.into ?? 'local';
98
130
  const mainFile = db.prepare("SELECT file FROM pragma_database_list WHERE name = 'main'").get().file;
99
- if (mainFile !== '' && realpathSync(sourcePath) === realpathSync(mainFile)) {
131
+ const sameFile = () => {
132
+ if (mainFile === '')
133
+ return false;
134
+ if (realpathSync(sourcePath) === realpathSync(mainFile))
135
+ return true;
136
+ const source = statSync(sourcePath);
137
+ const main = statSync(mainFile);
138
+ return source.dev === main.dev && source.ino === main.ino;
139
+ };
140
+ if (sameFile()) {
100
141
  // Attaching the store to itself would deadlock on its own lock; a
101
142
  // store trivially holds every row it holds.
102
143
  return { merged: 0, skipped: 0, edges: 0, dryRun, problems: [] };
103
144
  }
145
+ // SQLite cannot detach an attachment read by an uncommitted outer transaction.
146
+ store.transaction(({ outermost }) => {
147
+ if (!outermost) {
148
+ throw new Error('database sync cannot run inside a caller-owned transaction; sync after it commits or use annotated-text sync');
149
+ }
150
+ });
104
151
  try {
105
152
  db.prepare('ATTACH DATABASE ? AS cave_sync_src').run(sourcePath);
106
153
  }
107
- catch {
108
- throw new Error(`${sourcePath}: not a CAVE store (not a SQLite database)`);
154
+ catch (cause) {
155
+ throw new Error(`${sourcePath}: cannot attach sync source: ${errorMessage(cause)}`, { cause });
109
156
  }
110
- try {
157
+ return withSyncCleanup(() => {
111
158
  let isStore = false;
112
159
  try {
113
160
  isStore = db.prepare("SELECT 1 FROM cave_sync_src.sqlite_master WHERE type = 'table' AND name = 'cave_claim'").get() !== undefined;
114
161
  }
115
- catch {
116
- isStore = false;
162
+ catch (cause) {
163
+ throw new Error(`${sourcePath}: cannot inspect sync source: ${errorMessage(cause)}`, { cause });
117
164
  }
118
165
  if (!isStore) {
119
166
  throw new Error(`${sourcePath}: not a CAVE store (no cave_claim table)`);
@@ -123,20 +170,150 @@ export const syncDb = (store, sourcePath, options = {}) => {
123
170
  throw new Error(`${sourcePath}: schema version ${sourceVersion} is newer than this runtime supports ` +
124
171
  `(${Schema.currentVersion}); upgrade CAVE`);
125
172
  }
126
- if (sourceVersion === Schema.currentVersion) {
173
+ if (sourceVersion >= 1) {
127
174
  try {
128
175
  Schema.validate(db, sourceVersion, 'cave_sync_src');
129
176
  }
130
177
  catch (error) {
131
- throw new Error(`${sourcePath}: ${error instanceof Error ? error.message : String(error)}`);
178
+ throw new Error(`${sourcePath}: ${errorMessage(error)}`, { cause: error });
132
179
  }
133
180
  }
181
+ const hasProvenance = db.prepare(`
182
+ SELECT 1 FROM cave_sync_src.sqlite_master
183
+ WHERE type = 'table' AND name = 'cave_provenance'
184
+ `).get() !== undefined;
134
185
  const body = () => {
186
+ const references = [
187
+ ['cave_context', ['claim_id']], ['cave_tag', ['claim_id']],
188
+ ['cave_edge', ['parent_id', 'child_id']],
189
+ ...(hasProvenance ? [['cave_provenance', ['claim_id']]] : [])
190
+ ];
191
+ for (const [table, fields] of references) {
192
+ // Validate source relationships independently of what the target contains.
193
+ // Explicit queries also cover legacy tables without declared foreign keys.
194
+ const missing = fields.map(field => `NOT EXISTS (SELECT 1 FROM cave_sync_src.cave_claim c WHERE c.id = s.${field})`).join(' OR ');
195
+ if (db.prepare(`SELECT 1 FROM cave_sync_src.${table} s WHERE ${missing} LIMIT 1`).get() !== undefined) {
196
+ return { merged: 0, skipped: 0, edges: 0, dryRun, problems: [{ line: 0,
197
+ message: `source ${table} contains references to missing claims` }] };
198
+ }
199
+ }
200
+ if (db.prepare(`SELECT 1 FROM cave_sync_src.cave_edge
201
+ WHERE role IS NULL OR role NOT IN ('WHEN', 'VIA', 'BECAUSE', 'QUALIFIES') LIMIT 1`).get() !== undefined) {
202
+ return { merged: 0, skipped: 0, edges: 0, dryRun, problems: [{ line: 0,
203
+ message: 'source contains an invalid edge role — expected WHEN, VIA, BECAUSE or QUALIFIES' }] };
204
+ }
205
+ const identities = db.prepare('SELECT id, tx FROM cave_sync_src.cave_claim ORDER BY id').all();
206
+ const invalid = identities.filter(row => !Uuidv7.is(row.id) || row.tx !== row.id);
207
+ if (invalid.length > 0) {
208
+ return { merged: 0, skipped: 0, edges: 0, dryRun, problems: invalid.map(row => ({
209
+ line: 0,
210
+ message: `row ${JSON.stringify(row.id)} has invalid transaction identity — expected id = tx as a canonical lowercase UUIDv7 (spec §28.1)`,
211
+ })) };
212
+ }
213
+ if (hasProvenance) {
214
+ const malformed = db.prepare(`
215
+ SELECT DISTINCT claim_id AS id FROM cave_sync_src.cave_provenance
216
+ WHERE typeof(value) <> 'text' OR value = '' ORDER BY claim_id
217
+ `).all();
218
+ if (malformed.length > 0) {
219
+ return { merged: 0, skipped: 0, edges: 0, dryRun, problems: malformed.map(row => ({
220
+ line: 0,
221
+ message: `row ${JSON.stringify(row.id)} has invalid stored provenance — values must be nonempty text`,
222
+ })) };
223
+ }
224
+ }
225
+ // Compare persisted claim data under the same reservation as the copy.
226
+ // Raw spelling can change during text replay; metadata order is immaterial.
227
+ // Keep normalized value fields and claim_key in the comparison: divergent
228
+ // query data must not hide behind equal emitted text.
229
+ const columns = claimColumns.split(', ').filter(column => column !== 'raw_line');
230
+ const fieldsDiffer = columns.map(column => column === 'sigma_level' ?
231
+ 'COALESCE(s.sigma_level, 2) IS NOT COALESCE(t.sigma_level, 2)' :
232
+ `s.${column} IS NOT t.${column}`).join(' OR ');
233
+ const setsDiffer = (table, fields) => `
234
+ EXISTS (SELECT ${fields} FROM cave_sync_src.${table} WHERE claim_id = s.id
235
+ EXCEPT SELECT ${fields} FROM main.${table} WHERE claim_id = t.id)
236
+ OR EXISTS (SELECT ${fields} FROM main.${table} WHERE claim_id = t.id
237
+ EXCEPT SELECT ${fields} FROM cave_sync_src.${table} WHERE claim_id = s.id)`;
238
+ const conflicts = db.prepare(`
239
+ SELECT s.id FROM cave_sync_src.cave_claim s
240
+ JOIN main.cave_claim t ON t.id = s.id
241
+ WHERE ${fieldsDiffer}
242
+ OR ${setsDiffer('cave_context', 'context')}
243
+ OR ${setsDiffer('cave_tag', 'key, value')}
244
+ ORDER BY s.id
245
+ `).all();
246
+ if (hasProvenance) {
247
+ // Sync/backfill can add safely inferred dimensions. Ignore only those
248
+ // differences; explicit provenance on the same identity must agree.
249
+ const differences = db.prepare(`
250
+ SELECT claim_id, dimension, value FROM (
251
+ SELECT p.claim_id, p.dimension, p.value FROM cave_sync_src.cave_provenance p
252
+ JOIN main.cave_claim t ON t.id = p.claim_id
253
+ EXCEPT SELECT claim_id, dimension, value FROM main.cave_provenance
254
+ )
255
+ UNION
256
+ SELECT claim_id, dimension, value FROM (
257
+ SELECT p.claim_id, p.dimension, p.value FROM main.cave_provenance p
258
+ JOIN cave_sync_src.cave_claim s ON s.id = p.claim_id
259
+ EXCEPT SELECT claim_id, dimension, value FROM cave_sync_src.cave_provenance
260
+ )
261
+ ORDER BY claim_id
262
+ `).all();
263
+ const contextsOf = db.prepare('SELECT context FROM main.cave_context WHERE claim_id = ?');
264
+ const inferred = new Map();
265
+ const ids = new Set(conflicts.map(row => row.id));
266
+ for (const difference of differences) {
267
+ let entries = inferred.get(difference.claim_id);
268
+ if (entries === undefined) {
269
+ const contexts = contextsOf.all(difference.claim_id);
270
+ entries = new Set(Provenance.entries(contexts.map(row => row.context))
271
+ .map(entry => JSON.stringify([entry.dimension, entry.value])));
272
+ inferred.set(difference.claim_id, entries);
273
+ }
274
+ if (!entries.has(JSON.stringify([difference.dimension, difference.value])) && !ids.has(difference.claim_id)) {
275
+ ids.add(difference.claim_id);
276
+ conflicts.push({ id: difference.claim_id });
277
+ }
278
+ }
279
+ }
280
+ if (conflicts.length > 0) {
281
+ conflicts.sort((a, b) => a.id.localeCompare(b.id));
282
+ return { merged: 0, skipped: 0, edges: 0, dryRun, problems: conflicts.map(({ id }) => ({
283
+ line: 0,
284
+ message: `transaction id ${id} is already stored with different content — a row has one identity (spec §28.1)`,
285
+ })) };
286
+ }
287
+ const sourceClaims = db.prepare('SELECT * FROM cave_sync_src.cave_claim WHERE id > ? ORDER BY id LIMIT 512');
288
+ const sourceContexts = db.prepare('SELECT context FROM cave_sync_src.cave_context WHERE claim_id = ?');
289
+ const sourceTags = db.prepare('SELECT key, value FROM cave_sync_src.cave_tag WHERE claim_id = ?');
290
+ let after = '';
291
+ for (;;) {
292
+ const batch = sourceClaims.all(after);
293
+ if (batch.length === 0)
294
+ break;
295
+ const problems = [];
296
+ for (const row of batch) {
297
+ const contexts = sourceContexts.all(row.id).map(entry => entry.context);
298
+ const tags = sourceTags.all(row.id);
299
+ try {
300
+ const claim = Row.toClaim(row, contexts, tags);
301
+ Canonical.emitClaim(claim);
302
+ if (Key.of(claim) === row.claim_key)
303
+ continue;
304
+ }
305
+ catch { /* Report malformed stored data without exposing claim contents. */ }
306
+ problems.push({ line: 0, message: `row ${JSON.stringify(row.id)} has an invalid stored claim or inconsistent semantic key` });
307
+ }
308
+ if (problems.length > 0)
309
+ return { merged: 0, skipped: 0, edges: 0, dryRun, problems };
310
+ after = batch[batch.length - 1].id;
311
+ }
135
312
  db.exec('DROP TABLE IF EXISTS temp.cave_sync_new');
136
313
  db.exec(`CREATE TEMP TABLE cave_sync_new AS
137
314
  SELECT id FROM cave_sync_src.cave_claim
138
315
  WHERE id NOT IN (SELECT id FROM main.cave_claim)`);
139
- try {
316
+ return withSyncCleanup(() => {
140
317
  const count = (sql) => db.prepare(sql).get().n;
141
318
  const total = count('SELECT COUNT(*) AS n FROM cave_sync_src.cave_claim');
142
319
  const merged = count('SELECT COUNT(*) AS n FROM temp.cave_sync_new');
@@ -146,26 +323,27 @@ export const syncDb = (store, sourcePath, options = {}) => {
146
323
  db.exec(`INSERT INTO main.cave_context (claim_id, context)
147
324
  SELECT claim_id, context FROM cave_sync_src.cave_context
148
325
  WHERE claim_id IN (SELECT id FROM temp.cave_sync_new)`);
149
- const hasProvenance = db.prepare(`
150
- SELECT 1 FROM cave_sync_src.sqlite_master
151
- WHERE type = 'table' AND name = 'cave_provenance'
152
- `).get() !== undefined;
153
326
  if (hasProvenance) {
154
327
  db.exec(`INSERT OR IGNORE INTO main.cave_provenance (claim_id, dimension, value)
155
328
  SELECT claim_id, dimension, value FROM cave_sync_src.cave_provenance
156
329
  WHERE claim_id IN (SELECT id FROM temp.cave_sync_new)`);
157
330
  }
158
- // Older stores have no dimension table. Derive every safely
159
- // inferable dimension from their compact contexts after copying;
160
- // INSERT OR IGNORE also fills gaps in partially upgraded stores.
161
- const contextRows = db.prepare(`
331
+ // Only legacy stores without a dimension table need inference.
332
+ // A present table is authoritative, including explicit empty sets.
333
+ const contextRows = hasProvenance ? [] : db.prepare(`
162
334
  SELECT n.id, ctx.context FROM temp.cave_sync_new n
163
335
  LEFT JOIN main.cave_context ctx ON ctx.claim_id = n.id
164
336
  ORDER BY n.id, ctx.rowid
165
337
  `).all();
166
338
  const contexts = new Map();
167
339
  for (const row of contextRows) {
168
- contexts.set(row.id, [...contexts.get(row.id) ?? [], ...row.context === null ? [] : [row.context]]);
340
+ let values = contexts.get(row.id);
341
+ if (values === undefined) {
342
+ values = [];
343
+ contexts.set(row.id, values);
344
+ }
345
+ if (row.context !== null)
346
+ values.push(row.context);
169
347
  }
170
348
  const insertProvenance = db.prepare(`
171
349
  INSERT OR IGNORE INTO main.cave_provenance (claim_id, dimension, value) VALUES (?, ?, ?)
@@ -204,8 +382,8 @@ export const syncDb = (store, sourcePath, options = {}) => {
204
382
  // dry runs and failures.
205
383
  store.reloadRegistry();
206
384
  const effective = merged + edges > 0;
207
- const record = effective && options.record !== false ?
208
- appendRecord(store, options.from ?? 'origin', options.into ?? 'local', merged, edges) :
385
+ const record = effective && recordEnabled ?
386
+ appendRecord(store, from, into, merged, edges) :
209
387
  undefined;
210
388
  return {
211
389
  merged,
@@ -215,18 +393,12 @@ export const syncDb = (store, sourcePath, options = {}) => {
215
393
  ...record === undefined ? {} : { record },
216
394
  problems: []
217
395
  };
218
- }
219
- finally {
220
- db.exec('DROP TABLE IF EXISTS temp.cave_sync_new');
221
- }
396
+ }, () => db.exec('DROP TABLE IF EXISTS temp.cave_sync_new'));
222
397
  };
223
398
  return dryRun ?
224
399
  Uuidv7.withStatePreserved(() => rolledBack(store, body)) :
225
400
  store.transaction(body);
226
- }
227
- finally {
228
- db.exec('DETACH DATABASE cave_sync_src');
229
- }
401
+ }, () => db.exec('DETACH DATABASE cave_sync_src'));
230
402
  };
231
403
  /**
232
404
  * Merges §28.4 transaction-annotated canonical text (`cave export --tx`)
@@ -243,25 +415,71 @@ export const syncDb = (store, sourcePath, options = {}) => {
243
415
  * row, contributing its edge.
244
416
  */
245
417
  export const syncText = (store, text, options = {}) => {
246
- const dryRun = options.dryRun === true;
418
+ const dryRun = booleanOption(options.dryRun, 'dryRun', false);
419
+ const recordOptions = { record: booleanOption(options.record, 'record', true), from: options.from, into: options.into };
420
+ const body = () => syncTextReserved(store, text, recordOptions, dryRun);
421
+ return dryRun ?
422
+ Uuidv7.withStatePreserved(() => rolledBack(store, body)) :
423
+ store.transaction(body);
424
+ };
425
+ /** Compare canonical interchange content, independent of raw spelling and metadata order. */
426
+ const identityContent = (claim) => Canonical.emitClaim({
427
+ ...claim,
428
+ contexts: [...claim.contexts].sort(),
429
+ tags: [...claim.tags].sort((a, b) => {
430
+ const left = JSON.stringify([a.key, a.value]);
431
+ const right = JSON.stringify([b.key, b.value]);
432
+ return left < right ? -1 : left > right ? 1 : 0;
433
+ }),
434
+ });
435
+ /** Validation and identity replay use the same reserved vocabulary snapshot. */
436
+ const syncTextReserved = (store, text, options, dryRun) => {
247
437
  const problems = [];
248
438
  const txByLine = new Map();
439
+ const provenanceByLine = new Map();
249
440
  text.split(/\r?\n/).forEach((raw, at) => {
250
441
  const tx = Canonical.txOfLine(raw);
251
442
  if (tx === undefined) {
443
+ // A malformed annotation must not become comment prose merely because
444
+ // a later valid annotation supplies the claim's identity.
445
+ if (/^\s*;@/.test(raw)) {
446
+ problems.push({ line: at + 1, message: 'malformed transaction annotation — expected `;@ <uuidv7>` with optional provenance JSON (spec §28.4)' });
447
+ }
252
448
  return;
253
449
  }
254
450
  if (Uuidv7.is(tx)) {
255
451
  txByLine.set(at + 1, tx);
452
+ const data = Canonical.txDataOfLine(raw);
453
+ if (data !== undefined) {
454
+ let provenance;
455
+ try {
456
+ const payload = JSON.parse(data);
457
+ if (payload !== null && typeof payload === 'object' && !Array.isArray(payload) &&
458
+ Object.keys(payload).length === 1 && Object.hasOwn(payload, 'provenance')) {
459
+ provenance = Provenance.parse(payload.provenance);
460
+ }
461
+ }
462
+ catch { /* Report malformed JSON alongside other source problems. */ }
463
+ if (provenance === undefined) {
464
+ problems.push({ line: at + 1, message: 'malformed provenance annotation — expected a JSON object with complete provenance dimensions (spec §28.4)' });
465
+ }
466
+ else {
467
+ provenanceByLine.set(at + 1, provenance);
468
+ }
469
+ }
256
470
  }
257
471
  else {
258
472
  problems.push({ line: at + 1, message: `malformed transaction annotation — expected \`;@ <uuidv7>\` (spec §28.4)` });
259
473
  }
260
474
  });
261
475
  const result = Canonical.canonicalizeText(text, store.registry());
262
- problems.push(...result.problems);
476
+ for (const problem of result.problems)
477
+ problems.push(problem);
263
478
  const consumed = new Set();
264
479
  const seen = new Map();
480
+ const newProvenance = new Map();
481
+ const existingById = store.db.prepare('SELECT * FROM cave_claim WHERE id = ?');
482
+ const stored = new Map();
265
483
  const ids = result.claims.map(entry => {
266
484
  const line = entry.line - 1;
267
485
  const tx = txByLine.get(line);
@@ -277,13 +495,33 @@ export const syncText = (store, text, options = {}) => {
277
495
  // cited by several parents (§28.4) — and unions back into one row, the
278
496
  // same §28.1 rule that makes re-syncs idempotent. Only a repeat that
279
497
  // disagrees on content forks identity, and rejects.
280
- const rendered = Canonical.emitClaim(entry.claim);
498
+ const rendered = identityContent(entry.claim);
499
+ const explicitProvenance = provenanceByLine.get(line);
500
+ const provenance = JSON.stringify(explicitProvenance ??
501
+ Provenance.normalize(Provenance.fromEntries(Provenance.entries(entry.claim.contexts))));
281
502
  const first = seen.get(tx);
282
- if (first !== undefined && first.rendered !== rendered) {
503
+ if (first !== undefined && (first.rendered !== rendered || first.provenance !== provenance)) {
283
504
  problems.push({ line: entry.line, message: `transaction annotation repeats line ${first.line}'s id with different content — a row has one identity (spec §28.1)` });
284
505
  }
285
506
  else if (first === undefined) {
286
- seen.set(tx, { line: entry.line, rendered });
507
+ seen.set(tx, { line: entry.line, rendered, provenance });
508
+ }
509
+ // Validation runs under one reservation and precedes all writes. Repeated
510
+ // graph references can reuse the stored identity, including absent rows.
511
+ let existing = stored.get(tx);
512
+ if (!stored.has(tx)) {
513
+ const row = existingById.get(tx);
514
+ existing = row === undefined ? undefined : { rendered: identityContent(store.toClaim(row)) };
515
+ stored.set(tx, existing);
516
+ }
517
+ if (existing === undefined && explicitProvenance !== undefined)
518
+ newProvenance.set(tx, explicitProvenance);
519
+ if (existing !== undefined && existing.rendered === rendered && explicitProvenance !== undefined && existing.provenance === undefined) {
520
+ existing.provenance = JSON.stringify(Provenance.normalize(store.provenanceOf(tx)));
521
+ }
522
+ if (existing !== undefined && (existing.rendered !== rendered ||
523
+ (explicitProvenance !== undefined && existing.provenance !== provenance))) {
524
+ problems.push({ line: entry.line, message: `transaction id ${tx} is already stored with different content — a row has one identity (spec §28.1)` });
287
525
  }
288
526
  return tx;
289
527
  });
@@ -295,25 +533,31 @@ export const syncText = (store, text, options = {}) => {
295
533
  if (problems.length > 0) {
296
534
  return { merged: 0, skipped: 0, edges: 0, dryRun, problems: problems.sort((a, b) => a.line - b.line) };
297
535
  }
298
- const body = () => {
299
- const outcome = store.insertResult(result, { ids });
300
- const merged = result.claims.length - outcome.skipped;
301
- const effective = merged + outcome.edges > 0;
302
- const record = effective && options.record !== false ?
303
- appendRecord(store, options.from ?? 'origin', options.into ?? 'local', merged, outcome.edges) :
304
- undefined;
305
- return {
306
- merged,
307
- skipped: outcome.skipped,
308
- edges: outcome.edges,
309
- dryRun,
310
- ...record === undefined ? {} : { record },
311
- problems: []
312
- };
536
+ const outcome = store.insertResult(result, { ids });
537
+ // Explicit annotation dimensions replace legacy inference only for newly
538
+ // inserted identities. All validation above precedes any mutation.
539
+ const clearProvenance = store.db.prepare('DELETE FROM cave_provenance WHERE claim_id = ?');
540
+ const insertProvenance = store.db.prepare('INSERT INTO cave_provenance (claim_id, dimension, value) VALUES (?, ?, ?)');
541
+ for (const [id, value] of newProvenance) {
542
+ clearProvenance.run(id);
543
+ for (const [key, dimension] of [['actors', 'actor'], ['sources', 'source'], ['runs', 'run'], ['domains', 'domain']]) {
544
+ for (const entry of value[key])
545
+ insertProvenance.run(id, dimension, entry);
546
+ }
547
+ }
548
+ const merged = result.claims.length - outcome.skipped;
549
+ const effective = merged + outcome.edges > 0;
550
+ const record = effective && options.record !== false ?
551
+ appendRecord(store, options.from ?? 'origin', options.into ?? 'local', merged, outcome.edges) :
552
+ undefined;
553
+ return {
554
+ merged,
555
+ skipped: outcome.skipped,
556
+ edges: outcome.edges,
557
+ dryRun,
558
+ ...record === undefined ? {} : { record },
559
+ problems: []
313
560
  };
314
- return dryRun ?
315
- Uuidv7.withStatePreserved(() => rolledBack(store, body)) :
316
- store.transaction(body);
317
561
  };
318
562
  /**
319
563
  * Merges a source file of either shape: a CAVE store (recognized by the
@@ -322,9 +566,22 @@ export const syncText = (store, text, options = {}) => {
322
566
  * basename stem.
323
567
  */
324
568
  export const syncFile = (store, sourcePath, options = {}) => {
325
- const withLabel = { from: labelOf(sourcePath), ...options };
326
- return isStoreFile(sourcePath) ?
327
- syncDb(store, sourcePath, withLabel) :
328
- syncText(store, readFileSync(sourcePath, 'utf8'), withLabel);
569
+ const withLabel = {
570
+ from: options.from ?? labelOf(sourcePath),
571
+ into: options.into,
572
+ record: booleanOption(options.record, 'record', true),
573
+ dryRun: booleanOption(options.dryRun, 'dryRun', false)
574
+ };
575
+ if (isStoreFile(sourcePath))
576
+ return syncDb(store, sourcePath, withLabel);
577
+ const bytes = readFileSync(sourcePath);
578
+ let text;
579
+ try {
580
+ text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
581
+ }
582
+ catch (cause) {
583
+ throw new TypeError(`${sourcePath}: invalid UTF-8 sync text`, { cause });
584
+ }
585
+ return syncText(store, text, withLabel);
329
586
  };
330
587
  //# sourceMappingURL=sync.js.map