@askdkc/kiokuko 0.1.16 → 0.1.19

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 (549) hide show
  1. package/README.ja.md +91 -112
  2. package/README.ko.md +90 -102
  3. package/README.md +103 -260
  4. package/README.zh-CN.md +88 -102
  5. package/dist/agent-file/atomic-write.d.ts +90 -3
  6. package/dist/agent-file/atomic-write.d.ts.map +1 -1
  7. package/dist/agent-file/atomic-write.js +990 -14
  8. package/dist/agent-file/atomic-write.js.map +1 -1
  9. package/dist/agent-file/managed-block.d.ts +12 -1
  10. package/dist/agent-file/managed-block.d.ts.map +1 -1
  11. package/dist/agent-file/managed-block.js +49 -4
  12. package/dist/agent-file/managed-block.js.map +1 -1
  13. package/dist/agent-file/render.d.ts +1 -1
  14. package/dist/agent-file/render.d.ts.map +1 -1
  15. package/dist/agent-file/render.js +10 -7
  16. package/dist/agent-file/render.js.map +1 -1
  17. package/dist/akinator/agent-task.d.ts +14 -24
  18. package/dist/akinator/agent-task.d.ts.map +1 -1
  19. package/dist/akinator/agent-task.js +460 -103
  20. package/dist/akinator/agent-task.js.map +1 -1
  21. package/dist/akinator/capabilities.d.ts +52 -7
  22. package/dist/akinator/capabilities.d.ts.map +1 -1
  23. package/dist/akinator/capabilities.js +226 -28
  24. package/dist/akinator/capabilities.js.map +1 -1
  25. package/dist/akinator/capability-binding.d.ts +11 -0
  26. package/dist/akinator/capability-binding.d.ts.map +1 -0
  27. package/dist/akinator/capability-binding.js +56 -0
  28. package/dist/akinator/capability-binding.js.map +1 -0
  29. package/dist/akinator/domain.js +2 -2
  30. package/dist/akinator/domain.js.map +1 -1
  31. package/dist/akinator/knowledge-path.d.ts.map +1 -1
  32. package/dist/akinator/knowledge-path.js +409 -39
  33. package/dist/akinator/knowledge-path.js.map +1 -1
  34. package/dist/akinator/orchestrator.d.ts +3 -4
  35. package/dist/akinator/orchestrator.d.ts.map +1 -1
  36. package/dist/akinator/orchestrator.js +1 -4
  37. package/dist/akinator/orchestrator.js.map +1 -1
  38. package/dist/akinator/service.d.ts +0 -4
  39. package/dist/akinator/service.d.ts.map +1 -1
  40. package/dist/akinator/service.js +19 -35
  41. package/dist/akinator/service.js.map +1 -1
  42. package/dist/akinator/skill-discovery-attempt.d.ts +20 -0
  43. package/dist/akinator/skill-discovery-attempt.d.ts.map +1 -0
  44. package/dist/akinator/skill-discovery-attempt.js +390 -0
  45. package/dist/akinator/skill-discovery-attempt.js.map +1 -0
  46. package/dist/akinator/store.d.ts.map +1 -1
  47. package/dist/akinator/store.js +107 -52
  48. package/dist/akinator/store.js.map +1 -1
  49. package/dist/akinator/types.d.ts +0 -12
  50. package/dist/akinator/types.d.ts.map +1 -1
  51. package/dist/bin/kiokuko.js +0 -0
  52. package/dist/cli.d.ts +2 -0
  53. package/dist/cli.d.ts.map +1 -1
  54. package/dist/cli.js +561 -144
  55. package/dist/cli.js.map +1 -1
  56. package/dist/client/server-client.d.ts +5 -3
  57. package/dist/client/server-client.d.ts.map +1 -1
  58. package/dist/client/server-client.js +486 -181
  59. package/dist/client/server-client.js.map +1 -1
  60. package/dist/commands/agent.d.ts +2 -0
  61. package/dist/commands/agent.d.ts.map +1 -1
  62. package/dist/commands/agent.js +861 -86
  63. package/dist/commands/agent.js.map +1 -1
  64. package/dist/commands/backup.d.ts +2 -1
  65. package/dist/commands/backup.d.ts.map +1 -1
  66. package/dist/commands/backup.js +64 -8
  67. package/dist/commands/backup.js.map +1 -1
  68. package/dist/commands/doctor.d.ts +5 -1
  69. package/dist/commands/doctor.d.ts.map +1 -1
  70. package/dist/commands/doctor.js +70 -26
  71. package/dist/commands/doctor.js.map +1 -1
  72. package/dist/commands/export.d.ts +0 -4
  73. package/dist/commands/export.d.ts.map +1 -1
  74. package/dist/commands/export.js +191 -32
  75. package/dist/commands/export.js.map +1 -1
  76. package/dist/commands/import.d.ts +10 -1
  77. package/dist/commands/import.d.ts.map +1 -1
  78. package/dist/commands/import.js +592 -113
  79. package/dist/commands/import.js.map +1 -1
  80. package/dist/commands/init.d.ts +19 -1
  81. package/dist/commands/init.d.ts.map +1 -1
  82. package/dist/commands/init.js +569 -22
  83. package/dist/commands/init.js.map +1 -1
  84. package/dist/commands/purge.d.ts.map +1 -1
  85. package/dist/commands/purge.js +3 -0
  86. package/dist/commands/purge.js.map +1 -1
  87. package/dist/commands/setup.d.ts +34 -8
  88. package/dist/commands/setup.d.ts.map +1 -1
  89. package/dist/commands/setup.js +418 -86
  90. package/dist/commands/setup.js.map +1 -1
  91. package/dist/commands/skills.d.ts +9 -0
  92. package/dist/commands/skills.d.ts.map +1 -0
  93. package/dist/commands/skills.js +317 -0
  94. package/dist/commands/skills.js.map +1 -0
  95. package/dist/commands/use.d.ts +10 -2
  96. package/dist/commands/use.d.ts.map +1 -1
  97. package/dist/commands/use.js +851 -73
  98. package/dist/commands/use.js.map +1 -1
  99. package/dist/config/paths.d.ts +11 -1
  100. package/dist/config/paths.d.ts.map +1 -1
  101. package/dist/config/paths.js +64 -29
  102. package/dist/config/paths.js.map +1 -1
  103. package/dist/config/project-config.d.ts +1 -0
  104. package/dist/config/project-config.d.ts.map +1 -1
  105. package/dist/config/project-config.js +70 -32
  106. package/dist/config/project-config.js.map +1 -1
  107. package/dist/context/broker.d.ts +30 -14
  108. package/dist/context/broker.d.ts.map +1 -1
  109. package/dist/context/broker.js +628 -150
  110. package/dist/context/broker.js.map +1 -1
  111. package/dist/context/delivery.d.ts +8 -22
  112. package/dist/context/delivery.d.ts.map +1 -1
  113. package/dist/context/delivery.js +204 -152
  114. package/dist/context/delivery.js.map +1 -1
  115. package/dist/context/feedback.d.ts +11 -12
  116. package/dist/context/feedback.d.ts.map +1 -1
  117. package/dist/context/feedback.js +250 -268
  118. package/dist/context/feedback.js.map +1 -1
  119. package/dist/context/origin.d.ts +9 -0
  120. package/dist/context/origin.d.ts.map +1 -0
  121. package/dist/context/origin.js +15 -0
  122. package/dist/context/origin.js.map +1 -0
  123. package/dist/context/ranking.d.ts +4 -13
  124. package/dist/context/ranking.d.ts.map +1 -1
  125. package/dist/context/ranking.js +21 -5
  126. package/dist/context/ranking.js.map +1 -1
  127. package/dist/context/recommendations.d.ts +0 -5
  128. package/dist/context/recommendations.d.ts.map +1 -1
  129. package/dist/context/recommendations.js +0 -1
  130. package/dist/context/recommendations.js.map +1 -1
  131. package/dist/context/run-state.d.ts +26 -0
  132. package/dist/context/run-state.d.ts.map +1 -0
  133. package/dist/context/run-state.js +399 -0
  134. package/dist/context/run-state.js.map +1 -0
  135. package/dist/context/scoped-broker.d.ts +19 -6
  136. package/dist/context/scoped-broker.d.ts.map +1 -1
  137. package/dist/context/scoped-broker.js +530 -131
  138. package/dist/context/scoped-broker.js.map +1 -1
  139. package/dist/context/selection-state.d.ts +19 -0
  140. package/dist/context/selection-state.d.ts.map +1 -0
  141. package/dist/context/selection-state.js +250 -0
  142. package/dist/context/selection-state.js.map +1 -0
  143. package/dist/db/adapter.d.ts +5 -3
  144. package/dist/db/adapter.d.ts.map +1 -1
  145. package/dist/db/adapter.js +6 -3
  146. package/dist/db/adapter.js.map +1 -1
  147. package/dist/db/capabilities.d.ts.map +1 -1
  148. package/dist/db/capabilities.js +34 -16
  149. package/dist/db/capabilities.js.map +1 -1
  150. package/dist/db/connection.d.ts +8 -0
  151. package/dist/db/connection.d.ts.map +1 -1
  152. package/dist/db/connection.js +55 -26
  153. package/dist/db/connection.js.map +1 -1
  154. package/dist/db/migrate.d.ts +28 -7
  155. package/dist/db/migrate.d.ts.map +1 -1
  156. package/dist/db/migrate.js +106 -78
  157. package/dist/db/migrate.js.map +1 -1
  158. package/dist/db/sqlite-retry.d.ts +11 -0
  159. package/dist/db/sqlite-retry.d.ts.map +1 -0
  160. package/dist/db/sqlite-retry.js +66 -0
  161. package/dist/db/sqlite-retry.js.map +1 -0
  162. package/dist/db/transaction.d.ts +13 -0
  163. package/dist/db/transaction.d.ts.map +1 -1
  164. package/dist/db/transaction.js +53 -24
  165. package/dist/db/transaction.js.map +1 -1
  166. package/dist/db/upgrade-backup.d.ts +58 -2
  167. package/dist/db/upgrade-backup.d.ts.map +1 -1
  168. package/dist/db/upgrade-backup.js +757 -28
  169. package/dist/db/upgrade-backup.js.map +1 -1
  170. package/dist/errors.js +1 -1
  171. package/dist/errors.js.map +1 -1
  172. package/dist/gateway/agent-service.d.ts +4 -1
  173. package/dist/gateway/agent-service.d.ts.map +1 -1
  174. package/dist/gateway/agent-service.js +71 -58
  175. package/dist/gateway/agent-service.js.map +1 -1
  176. package/dist/gateway/checkpoint-service.d.ts +1 -0
  177. package/dist/gateway/checkpoint-service.d.ts.map +1 -1
  178. package/dist/gateway/checkpoint-service.js +47 -24
  179. package/dist/gateway/checkpoint-service.js.map +1 -1
  180. package/dist/ledger/archive.d.ts +1 -4
  181. package/dist/ledger/archive.d.ts.map +1 -1
  182. package/dist/ledger/archive.js +134 -54
  183. package/dist/ledger/archive.js.map +1 -1
  184. package/dist/ledger/maintenance.d.ts +0 -11
  185. package/dist/ledger/maintenance.d.ts.map +1 -1
  186. package/dist/ledger/maintenance.js +41 -44
  187. package/dist/ledger/maintenance.js.map +1 -1
  188. package/dist/ledger/projection.d.ts +0 -1
  189. package/dist/ledger/projection.d.ts.map +1 -1
  190. package/dist/ledger/projection.js.map +1 -1
  191. package/dist/ledger/promotion.d.ts +0 -9
  192. package/dist/ledger/promotion.d.ts.map +1 -1
  193. package/dist/ledger/promotion.js.map +1 -1
  194. package/dist/ledger/query.d.ts +1 -15
  195. package/dist/ledger/query.d.ts.map +1 -1
  196. package/dist/ledger/query.js.map +1 -1
  197. package/dist/ledger/store.d.ts +0 -1
  198. package/dist/ledger/store.d.ts.map +1 -1
  199. package/dist/ledger/store.js +11 -8
  200. package/dist/ledger/store.js.map +1 -1
  201. package/dist/ledger/types.d.ts +0 -4
  202. package/dist/ledger/types.d.ts.map +1 -1
  203. package/dist/ledger/types.js.map +1 -1
  204. package/dist/ledger/validate.d.ts.map +1 -1
  205. package/dist/ledger/validate.js +0 -5
  206. package/dist/ledger/validate.js.map +1 -1
  207. package/dist/mcp/bounded-stdio-transport.d.ts +23 -0
  208. package/dist/mcp/bounded-stdio-transport.d.ts.map +1 -0
  209. package/dist/mcp/bounded-stdio-transport.js +109 -0
  210. package/dist/mcp/bounded-stdio-transport.js.map +1 -0
  211. package/dist/mcp/server.d.ts +4 -0
  212. package/dist/mcp/server.d.ts.map +1 -1
  213. package/dist/mcp/server.js +111 -75
  214. package/dist/mcp/server.js.map +1 -1
  215. package/dist/memory/audit.d.ts +0 -1
  216. package/dist/memory/audit.d.ts.map +1 -1
  217. package/dist/memory/audit.js +0 -1
  218. package/dist/memory/audit.js.map +1 -1
  219. package/dist/memory/curator.d.ts +45 -1
  220. package/dist/memory/curator.d.ts.map +1 -1
  221. package/dist/memory/curator.js +290 -68
  222. package/dist/memory/curator.js.map +1 -1
  223. package/dist/memory/entries.d.ts +12 -2
  224. package/dist/memory/entries.d.ts.map +1 -1
  225. package/dist/memory/entries.js +198 -114
  226. package/dist/memory/entries.js.map +1 -1
  227. package/dist/memory/federated-retrieval.d.ts +78 -0
  228. package/dist/memory/federated-retrieval.d.ts.map +1 -0
  229. package/dist/memory/federated-retrieval.js +441 -0
  230. package/dist/memory/federated-retrieval.js.map +1 -0
  231. package/dist/memory/hybrid-retrieval.d.ts +5 -11
  232. package/dist/memory/hybrid-retrieval.d.ts.map +1 -1
  233. package/dist/memory/hybrid-retrieval.js +78 -58
  234. package/dist/memory/hybrid-retrieval.js.map +1 -1
  235. package/dist/memory/lifecycle.d.ts.map +1 -1
  236. package/dist/memory/lifecycle.js +17 -13
  237. package/dist/memory/lifecycle.js.map +1 -1
  238. package/dist/memory/portability.d.ts +10 -0
  239. package/dist/memory/portability.d.ts.map +1 -0
  240. package/dist/memory/portability.js +44 -0
  241. package/dist/memory/portability.js.map +1 -0
  242. package/dist/memory/rebuild-search.d.ts +5 -0
  243. package/dist/memory/rebuild-search.d.ts.map +1 -1
  244. package/dist/memory/rebuild-search.js +101 -76
  245. package/dist/memory/rebuild-search.js.map +1 -1
  246. package/dist/memory/retrieval-query.d.ts +0 -5
  247. package/dist/memory/retrieval-query.d.ts.map +1 -1
  248. package/dist/memory/retrieval-query.js +0 -5
  249. package/dist/memory/retrieval-query.js.map +1 -1
  250. package/dist/memory/retrieval.d.ts +11 -0
  251. package/dist/memory/retrieval.d.ts.map +1 -1
  252. package/dist/memory/retrieval.js +34 -120
  253. package/dist/memory/retrieval.js.map +1 -1
  254. package/dist/memory/revisions.d.ts +76 -1
  255. package/dist/memory/revisions.d.ts.map +1 -1
  256. package/dist/memory/revisions.js +539 -35
  257. package/dist/memory/revisions.js.map +1 -1
  258. package/dist/memory/scoped-memory.d.ts +22 -13
  259. package/dist/memory/scoped-memory.d.ts.map +1 -1
  260. package/dist/memory/scoped-memory.js +248 -139
  261. package/dist/memory/scoped-memory.js.map +1 -1
  262. package/dist/memory/secrets.d.ts +2 -0
  263. package/dist/memory/secrets.d.ts.map +1 -1
  264. package/dist/memory/secrets.js +36 -0
  265. package/dist/memory/secrets.js.map +1 -1
  266. package/dist/memory/structured-memory.d.ts +8 -1
  267. package/dist/memory/structured-memory.d.ts.map +1 -1
  268. package/dist/memory/structured-memory.js +74 -20
  269. package/dist/memory/structured-memory.js.map +1 -1
  270. package/dist/memory/workspaces.d.ts +9 -0
  271. package/dist/memory/workspaces.d.ts.map +1 -1
  272. package/dist/memory/workspaces.js +67 -0
  273. package/dist/memory/workspaces.js.map +1 -1
  274. package/dist/package-version.d.ts +2 -0
  275. package/dist/package-version.d.ts.map +1 -0
  276. package/dist/package-version.js +7 -0
  277. package/dist/package-version.js.map +1 -0
  278. package/dist/repository/binding.d.ts +5 -0
  279. package/dist/repository/binding.d.ts.map +1 -1
  280. package/dist/repository/binding.js +117 -17
  281. package/dist/repository/binding.js.map +1 -1
  282. package/dist/repository/framework-version.d.ts +8 -0
  283. package/dist/repository/framework-version.d.ts.map +1 -0
  284. package/dist/repository/framework-version.js +29 -0
  285. package/dist/repository/framework-version.js.map +1 -0
  286. package/dist/repository/identity-value.d.ts +4 -0
  287. package/dist/repository/identity-value.d.ts.map +1 -0
  288. package/dist/repository/identity-value.js +24 -0
  289. package/dist/repository/identity-value.js.map +1 -0
  290. package/dist/repository/identity.d.ts.map +1 -1
  291. package/dist/repository/identity.js +2 -0
  292. package/dist/repository/identity.js.map +1 -1
  293. package/dist/repository/project-fingerprint.d.ts +33 -0
  294. package/dist/repository/project-fingerprint.d.ts.map +1 -0
  295. package/dist/repository/project-fingerprint.js +429 -0
  296. package/dist/repository/project-fingerprint.js.map +1 -0
  297. package/dist/repository/remote-url.d.ts +0 -1
  298. package/dist/repository/remote-url.d.ts.map +1 -1
  299. package/dist/repository/remote-url.js +0 -1
  300. package/dist/repository/remote-url.js.map +1 -1
  301. package/dist/runtime-version.d.ts +2 -1
  302. package/dist/runtime-version.d.ts.map +1 -1
  303. package/dist/runtime-version.js +21 -4
  304. package/dist/runtime-version.js.map +1 -1
  305. package/dist/serialization/boundary-json.d.ts +20 -0
  306. package/dist/serialization/boundary-json.d.ts.map +1 -0
  307. package/dist/serialization/boundary-json.js +129 -0
  308. package/dist/serialization/boundary-json.js.map +1 -0
  309. package/dist/serialization/envelope.js +1 -1
  310. package/dist/serialization/envelope.js.map +1 -1
  311. package/dist/serialization/validate.d.ts +7 -0
  312. package/dist/serialization/validate.d.ts.map +1 -1
  313. package/dist/serialization/validate.js +53 -13
  314. package/dist/serialization/validate.js.map +1 -1
  315. package/dist/server/app.js +1 -1
  316. package/dist/server/app.js.map +1 -1
  317. package/dist/server/http.d.ts +4 -3
  318. package/dist/server/http.d.ts.map +1 -1
  319. package/dist/server/http.js +90 -50
  320. package/dist/server/http.js.map +1 -1
  321. package/dist/server/idempotency.d.ts +0 -6
  322. package/dist/server/idempotency.d.ts.map +1 -1
  323. package/dist/server/idempotency.js.map +1 -1
  324. package/dist/server/router.d.ts.map +1 -1
  325. package/dist/server/router.js +21 -4
  326. package/dist/server/router.js.map +1 -1
  327. package/dist/server/routes/agent-capability-gate.d.ts +40 -0
  328. package/dist/server/routes/agent-capability-gate.d.ts.map +1 -0
  329. package/dist/server/routes/agent-capability-gate.js +182 -0
  330. package/dist/server/routes/agent-capability-gate.js.map +1 -0
  331. package/dist/server/routes/agent-events.d.ts.map +1 -1
  332. package/dist/server/routes/agent-events.js +8 -1
  333. package/dist/server/routes/agent-events.js.map +1 -1
  334. package/dist/server/routes/agent-intake.d.ts.map +1 -1
  335. package/dist/server/routes/agent-intake.js +10 -2
  336. package/dist/server/routes/agent-intake.js.map +1 -1
  337. package/dist/server/routes/agent-promotions.d.ts.map +1 -1
  338. package/dist/server/routes/agent-promotions.js +5 -2
  339. package/dist/server/routes/agent-promotions.js.map +1 -1
  340. package/dist/server/routes/agent-runs.d.ts.map +1 -1
  341. package/dist/server/routes/agent-runs.js +18 -4
  342. package/dist/server/routes/agent-runs.js.map +1 -1
  343. package/dist/server/routes/request-binding.d.ts +5 -0
  344. package/dist/server/routes/request-binding.d.ts.map +1 -0
  345. package/dist/server/routes/request-binding.js +49 -0
  346. package/dist/server/routes/request-binding.js.map +1 -0
  347. package/dist/server/routes/task5-support.d.ts +1 -8
  348. package/dist/server/routes/task5-support.d.ts.map +1 -1
  349. package/dist/server/routes/task5-support.js +1 -17
  350. package/dist/server/routes/task5-support.js.map +1 -1
  351. package/dist/server/routes/task5.d.ts.map +1 -1
  352. package/dist/server/routes/task5.js +106 -36
  353. package/dist/server/routes/task5.js.map +1 -1
  354. package/dist/server/write-queue.js +2 -2
  355. package/dist/server/write-queue.js.map +1 -1
  356. package/dist/setup/claude-config.d.ts +2 -1
  357. package/dist/setup/claude-config.d.ts.map +1 -1
  358. package/dist/setup/claude-config.js +54 -4
  359. package/dist/setup/claude-config.js.map +1 -1
  360. package/dist/setup/client-detection.d.ts +7 -2
  361. package/dist/setup/client-detection.d.ts.map +1 -1
  362. package/dist/setup/client-detection.js +27 -9
  363. package/dist/setup/client-detection.js.map +1 -1
  364. package/dist/setup/hermes-config.d.ts +2 -1
  365. package/dist/setup/hermes-config.d.ts.map +1 -1
  366. package/dist/setup/hermes-config.js +53 -12
  367. package/dist/setup/hermes-config.js.map +1 -1
  368. package/dist/setup/legacy-client-cleanup.d.ts +6 -0
  369. package/dist/setup/legacy-client-cleanup.d.ts.map +1 -0
  370. package/dist/setup/legacy-client-cleanup.js +152 -0
  371. package/dist/setup/legacy-client-cleanup.js.map +1 -0
  372. package/dist/setup/managed-text.d.ts.map +1 -1
  373. package/dist/setup/managed-text.js +11 -1
  374. package/dist/setup/managed-text.js.map +1 -1
  375. package/dist/setup/opencode-config.d.ts +2 -1
  376. package/dist/setup/opencode-config.d.ts.map +1 -1
  377. package/dist/setup/opencode-config.js +57 -5
  378. package/dist/setup/opencode-config.js.map +1 -1
  379. package/dist/setup/render.d.ts +2 -1
  380. package/dist/setup/render.d.ts.map +1 -1
  381. package/dist/setup/render.js +91 -17
  382. package/dist/setup/render.js.map +1 -1
  383. package/dist/setup/standard-skills.d.ts +16 -2
  384. package/dist/setup/standard-skills.d.ts.map +1 -1
  385. package/dist/setup/standard-skills.js +37 -13
  386. package/dist/setup/standard-skills.js.map +1 -1
  387. package/dist/setup/strict-json.d.ts +7 -0
  388. package/dist/setup/strict-json.d.ts.map +1 -0
  389. package/dist/setup/strict-json.js +98 -0
  390. package/dist/setup/strict-json.js.map +1 -0
  391. package/dist/setup/strict-toml.d.ts +7 -0
  392. package/dist/setup/strict-toml.d.ts.map +1 -0
  393. package/dist/setup/strict-toml.js +470 -0
  394. package/dist/setup/strict-toml.js.map +1 -0
  395. package/dist/skills/cache.d.ts +15 -0
  396. package/dist/skills/cache.d.ts.map +1 -0
  397. package/dist/skills/cache.js +19 -0
  398. package/dist/skills/cache.js.map +1 -0
  399. package/dist/skills/candidate-ranking.d.ts +15 -0
  400. package/dist/skills/candidate-ranking.d.ts.map +1 -0
  401. package/dist/skills/candidate-ranking.js +52 -0
  402. package/dist/skills/candidate-ranking.js.map +1 -0
  403. package/dist/skills/chunking.d.ts +11 -0
  404. package/dist/skills/chunking.d.ts.map +1 -0
  405. package/dist/skills/chunking.js +291 -0
  406. package/dist/skills/chunking.js.map +1 -0
  407. package/dist/skills/config.d.ts +19 -0
  408. package/dist/skills/config.d.ts.map +1 -0
  409. package/dist/skills/config.js +103 -0
  410. package/dist/skills/config.js.map +1 -0
  411. package/dist/skills/discovery-service.d.ts +26 -0
  412. package/dist/skills/discovery-service.d.ts.map +1 -0
  413. package/dist/skills/discovery-service.js +921 -0
  414. package/dist/skills/discovery-service.js.map +1 -0
  415. package/dist/skills/external-transport.d.ts +2 -0
  416. package/dist/skills/external-transport.d.ts.map +1 -0
  417. package/dist/skills/external-transport.js +30 -0
  418. package/dist/skills/external-transport.js.map +1 -0
  419. package/dist/skills/find.d.ts +16 -0
  420. package/dist/skills/find.d.ts.map +1 -0
  421. package/dist/skills/find.js +58 -0
  422. package/dist/skills/find.js.map +1 -0
  423. package/dist/skills/gap-detection.d.ts +23 -0
  424. package/dist/skills/gap-detection.d.ts.map +1 -0
  425. package/dist/skills/gap-detection.js +62 -0
  426. package/dist/skills/gap-detection.js.map +1 -0
  427. package/dist/skills/import-preparation.d.ts +4 -0
  428. package/dist/skills/import-preparation.d.ts.map +1 -0
  429. package/dist/skills/import-preparation.js +20 -0
  430. package/dist/skills/import-preparation.js.map +1 -0
  431. package/dist/skills/materialization-authority.d.ts +11 -0
  432. package/dist/skills/materialization-authority.d.ts.map +1 -0
  433. package/dist/skills/materialization-authority.js +92 -0
  434. package/dist/skills/materialization-authority.js.map +1 -0
  435. package/dist/skills/materialization-service.d.ts +22 -0
  436. package/dist/skills/materialization-service.d.ts.map +1 -0
  437. package/dist/skills/materialization-service.js +101 -0
  438. package/dist/skills/materialization-service.js.map +1 -0
  439. package/dist/skills/official-catalog.d.ts +31 -0
  440. package/dist/skills/official-catalog.d.ts.map +1 -0
  441. package/dist/skills/official-catalog.js +110 -0
  442. package/dist/skills/official-catalog.js.map +1 -0
  443. package/dist/skills/providers/schema.d.ts +30 -0
  444. package/dist/skills/providers/schema.d.ts.map +1 -0
  445. package/dist/skills/providers/schema.js +354 -0
  446. package/dist/skills/providers/schema.js.map +1 -0
  447. package/dist/skills/providers/skills-sh-compat.d.ts +17 -0
  448. package/dist/skills/providers/skills-sh-compat.d.ts.map +1 -0
  449. package/dist/skills/providers/skills-sh-compat.js +71 -0
  450. package/dist/skills/providers/skills-sh-compat.js.map +1 -0
  451. package/dist/skills/providers/skills-sh-v1.d.ts +23 -0
  452. package/dist/skills/providers/skills-sh-v1.d.ts.map +1 -0
  453. package/dist/skills/providers/skills-sh-v1.js +134 -0
  454. package/dist/skills/providers/skills-sh-v1.js.map +1 -0
  455. package/dist/skills/query-builder.d.ts +18 -0
  456. package/dist/skills/query-builder.d.ts.map +1 -0
  457. package/dist/skills/query-builder.js +93 -0
  458. package/dist/skills/query-builder.js.map +1 -0
  459. package/dist/skills/source/errors.d.ts +9 -0
  460. package/dist/skills/source/errors.d.ts.map +1 -0
  461. package/dist/skills/source/errors.js +15 -0
  462. package/dist/skills/source/errors.js.map +1 -0
  463. package/dist/skills/source/frontmatter.d.ts +9 -0
  464. package/dist/skills/source/frontmatter.d.ts.map +1 -0
  465. package/dist/skills/source/frontmatter.js +77 -0
  466. package/dist/skills/source/frontmatter.js.map +1 -0
  467. package/dist/skills/source/github-fetcher.d.ts +20 -0
  468. package/dist/skills/source/github-fetcher.d.ts.map +1 -0
  469. package/dist/skills/source/github-fetcher.js +292 -0
  470. package/dist/skills/source/github-fetcher.js.map +1 -0
  471. package/dist/skills/source/snapshot-validator.d.ts +28 -0
  472. package/dist/skills/source/snapshot-validator.d.ts.map +1 -0
  473. package/dist/skills/source/snapshot-validator.js +186 -0
  474. package/dist/skills/source/snapshot-validator.js.map +1 -0
  475. package/dist/skills/store.d.ts +181 -0
  476. package/dist/skills/store.d.ts.map +1 -0
  477. package/dist/skills/store.js +1710 -0
  478. package/dist/skills/store.js.map +1 -0
  479. package/dist/skills/types.d.ts +169 -0
  480. package/dist/skills/types.d.ts.map +1 -0
  481. package/dist/skills/types.js +2 -0
  482. package/dist/skills/types.js.map +1 -0
  483. package/dist/web/i18n.d.ts +56 -8
  484. package/dist/web/i18n.d.ts.map +1 -1
  485. package/dist/web/i18n.js +224 -32
  486. package/dist/web/i18n.js.map +1 -1
  487. package/dist/web/server.d.ts.map +1 -1
  488. package/dist/web/server.js +458 -42
  489. package/dist/web/server.js.map +1 -1
  490. package/dist/web/ui.d.ts +4 -0
  491. package/dist/web/ui.d.ts.map +1 -1
  492. package/dist/web/ui.js +697 -61
  493. package/dist/web/ui.js.map +1 -1
  494. package/migrations/008_federated_memory.sql +46 -0
  495. package/migrations/009_external_skill_discovery.sql +126 -0
  496. package/package.json +6 -3
  497. package/skills/kiokuko-single-purpose-functions/SKILL.md +288 -0
  498. package/skills/kiokuko-single-purpose-functions/references/kiokuko-patterns.md +344 -0
  499. package/skills/kiokuko-single-purpose-functions/references/review-checklist.md +97 -0
  500. package/skills/kiokuko-ui-design-soul/SKILL.md +1186 -28
  501. package/templates/AGENTS.md +8 -7
  502. package/dist/client/runtime-discovery.d.ts +0 -15
  503. package/dist/client/runtime-discovery.d.ts.map +0 -1
  504. package/dist/client/runtime-discovery.js +0 -45
  505. package/dist/client/runtime-discovery.js.map +0 -1
  506. package/dist/commands/call.d.ts +0 -7
  507. package/dist/commands/call.d.ts.map +0 -1
  508. package/dist/commands/call.js +0 -10
  509. package/dist/commands/call.js.map +0 -1
  510. package/dist/commands/link.d.ts +0 -6
  511. package/dist/commands/link.d.ts.map +0 -1
  512. package/dist/commands/link.js +0 -6
  513. package/dist/commands/link.js.map +0 -1
  514. package/dist/commands/promote.d.ts +0 -6
  515. package/dist/commands/promote.d.ts.map +0 -1
  516. package/dist/commands/promote.js +0 -6
  517. package/dist/commands/promote.js.map +0 -1
  518. package/dist/commands/read.d.ts +0 -6
  519. package/dist/commands/read.d.ts.map +0 -1
  520. package/dist/commands/read.js +0 -4
  521. package/dist/commands/read.js.map +0 -1
  522. package/dist/commands/recall.d.ts +0 -6
  523. package/dist/commands/recall.d.ts.map +0 -1
  524. package/dist/commands/recall.js +0 -4
  525. package/dist/commands/recall.js.map +0 -1
  526. package/dist/commands/record.d.ts +0 -6
  527. package/dist/commands/record.d.ts.map +0 -1
  528. package/dist/commands/record.js +0 -4
  529. package/dist/commands/record.js.map +0 -1
  530. package/dist/commands/search.d.ts +0 -6
  531. package/dist/commands/search.d.ts.map +0 -1
  532. package/dist/commands/search.js +0 -4
  533. package/dist/commands/search.js.map +0 -1
  534. package/dist/commands/supersede.d.ts +0 -6
  535. package/dist/commands/supersede.d.ts.map +0 -1
  536. package/dist/commands/supersede.js +0 -6
  537. package/dist/commands/supersede.js.map +0 -1
  538. package/dist/knowledge/sources.d.ts +0 -72
  539. package/dist/knowledge/sources.d.ts.map +0 -1
  540. package/dist/knowledge/sources.js +0 -388
  541. package/dist/knowledge/sources.js.map +0 -1
  542. package/dist/setup/opencode-evidence.d.ts +0 -37
  543. package/dist/setup/opencode-evidence.d.ts.map +0 -1
  544. package/dist/setup/opencode-evidence.js +0 -58
  545. package/dist/setup/opencode-evidence.js.map +0 -1
  546. package/dist/setup/opencode-loop-guard.d.ts +0 -9
  547. package/dist/setup/opencode-loop-guard.d.ts.map +0 -1
  548. package/dist/setup/opencode-loop-guard.js +0 -248
  549. package/dist/setup/opencode-loop-guard.js.map +0 -1
@@ -1,36 +1,1012 @@
1
- import { chmod, lstat, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
1
+ import { constants } from 'node:fs';
2
+ import { chmod, link, lstat, mkdir, open, readdir, rename, unlink, } from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { randomUUID } from 'node:crypto';
5
+ import { TextDecoder } from 'node:util';
4
6
  import { KiokukoError } from '../errors.js';
5
- export async function readRegularFile(filePath) {
7
+ export class AtomicCleanupFailure extends Error {
8
+ artifactPath;
9
+ constructor(
10
+ /** Exact pathname of the owned artifact, when its full binding was proven after the failure. */
11
+ artifactPath, options) {
12
+ super('Atomic artifact cleanup failed or could not be proven complete', options);
13
+ this.artifactPath = artifactPath;
14
+ this.name = 'AtomicCleanupFailure';
15
+ }
16
+ }
17
+ export class AtomicCommittedMutationError extends AggregateError {
18
+ outcome;
19
+ operationError;
20
+ constructor(outcome, operationError) {
21
+ super([operationError, ...outcome.cleanupFailures], 'File target committed before post-install validation failed');
22
+ this.outcome = outcome;
23
+ this.operationError = operationError;
24
+ this.name = 'AtomicCommittedMutationError';
25
+ }
26
+ }
27
+ export class AtomicCommittedUnlinkError extends AggregateError {
28
+ outcome;
29
+ operationError;
30
+ constructor(outcome, operationError) {
31
+ super([operationError, ...outcome.cleanupFailures], 'File target was quarantined before post-unlink validation failed');
32
+ this.outcome = outcome;
33
+ this.operationError = operationError;
34
+ this.name = 'AtomicCommittedUnlinkError';
35
+ }
36
+ }
37
+ export function assertAtomicCleanupComplete(result) {
38
+ if (result.cleanupFailures.length === 0)
39
+ return;
40
+ throw new AggregateError(result.cleanupFailures, 'File mutation committed, but committed-artifact cleanup failed');
41
+ }
42
+ function errno(error) {
43
+ return error instanceof Error && 'code' in error
44
+ ? String(error.code)
45
+ : undefined;
46
+ }
47
+ function isMissingFile(error) {
48
+ return errno(error) === 'ENOENT';
49
+ }
50
+ function isSymlinkFailure(error) {
51
+ return ['ELOOP', 'EMLINK'].includes(errno(error) ?? '');
52
+ }
53
+ function isAlreadyExists(error) {
54
+ return errno(error) === 'EEXIST';
55
+ }
56
+ function changedAfterPlanning(filePath) {
57
+ return new KiokukoError('CONFLICT', 'Setup target changed after planning', filePath === undefined ? {} : { target: filePath });
58
+ }
59
+ function identityFromStat(info) {
60
+ const device = BigInt(info.dev);
61
+ const inode = BigInt(info.ino);
62
+ if (inode === 0n) {
63
+ throw new KiokukoError('SECURITY_REJECTION', 'The filesystem does not expose a stable file identity required for safe mutation');
64
+ }
65
+ return { device, inode };
66
+ }
67
+ function sameIdentity(left, right) {
68
+ return left.device === right.device && left.inode === right.inode;
69
+ }
70
+ function sameSnapshot(left, right) {
71
+ if (left === undefined || right === undefined)
72
+ return left === right;
73
+ return sameIdentity(left.identity, right.identity)
74
+ && left.content === right.content
75
+ && left.mode === right.mode;
76
+ }
77
+ function artifactFromOpenHandle(info, content) {
78
+ if (!info.isFile()) {
79
+ throw new KiokukoError('INTEGRITY_ERROR', 'Atomic artifact is not a regular file');
80
+ }
81
+ return {
82
+ snapshot: {
83
+ content,
84
+ mode: Number(info.mode) & 0o777,
85
+ identity: identityFromStat(info),
86
+ },
87
+ linkCount: BigInt(info.nlink),
88
+ };
89
+ }
90
+ async function readOpenHandleArtifact(handle, filePath) {
91
+ const before = await handle.stat();
92
+ if (!before.isFile() || !Number.isSafeInteger(before.size) || before.size < 0) {
93
+ throw new KiokukoError('INTEGRITY_ERROR', 'Atomic artifact cannot be read safely');
94
+ }
95
+ const buffer = Buffer.alloc(before.size);
96
+ let offset = 0;
97
+ while (offset < buffer.length) {
98
+ const result = await handle.read(buffer, offset, buffer.length - offset, offset);
99
+ if (result.bytesRead === 0) {
100
+ throw new KiokukoError('INTEGRITY_ERROR', 'Atomic artifact changed while it was read');
101
+ }
102
+ offset += result.bytesRead;
103
+ }
104
+ const after = await handle.stat();
105
+ const initial = artifactFromOpenHandle(before, decodeUtf8(buffer, filePath));
106
+ const final = artifactFromOpenHandle(after, initial.snapshot.content);
107
+ if (!sameArtifact(initial, final) || before.size !== after.size) {
108
+ throw new KiokukoError('INTEGRITY_ERROR', 'Atomic artifact changed while it was read');
109
+ }
110
+ return final;
111
+ }
112
+ function sameArtifact(left, right) {
113
+ return left !== undefined
114
+ && sameSnapshot(left.snapshot, right.snapshot)
115
+ && left.linkCount === right.linkCount;
116
+ }
117
+ function withLinkCount(artifact, linkCount) {
118
+ return { snapshot: artifact.snapshot, linkCount };
119
+ }
120
+ function requireArtifact(current, expected, message) {
121
+ if (current === undefined || !sameArtifact(current, expected)) {
122
+ throw new KiokukoError('INTEGRITY_ERROR', message);
123
+ }
124
+ return current;
125
+ }
126
+ function ensureContained(root, filePath) {
127
+ const resolvedRoot = path.resolve(root);
128
+ const target = path.resolve(filePath);
129
+ if (target === resolvedRoot || !target.startsWith(`${resolvedRoot}${path.sep}`)) {
130
+ throw new KiokukoError('VALIDATION_ERROR', 'Managed file must remain inside its repository root');
131
+ }
132
+ return { root: resolvedRoot, target };
133
+ }
134
+ async function rejectLinkedComponents(root, filePath) {
135
+ const contained = ensureContained(root, filePath);
136
+ const relative = path.relative(contained.root, contained.target);
137
+ let current = contained.root;
138
+ for (const component of relative.split(path.sep)) {
139
+ current = path.join(current, component);
140
+ try {
141
+ const info = await lstat(current);
142
+ if (info.isSymbolicLink()) {
143
+ throw new KiokukoError('SECURITY_REJECTION', 'Refusing to traverse a symbolic link');
144
+ }
145
+ if (current !== contained.target && !info.isDirectory()) {
146
+ throw new KiokukoError('VALIDATION_ERROR', `Expected a directory: ${current}`);
147
+ }
148
+ }
149
+ catch (error) {
150
+ if (isMissingFile(error))
151
+ return;
152
+ throw error;
153
+ }
154
+ }
155
+ }
156
+ function decodeUtf8(buffer, filePath) {
157
+ try {
158
+ return new TextDecoder('utf-8', { fatal: true }).decode(buffer);
159
+ }
160
+ catch {
161
+ throw new KiokukoError('VALIDATION_ERROR', `Managed text file is not valid UTF-8: ${filePath}`);
162
+ }
163
+ }
164
+ export async function readRegularFile(filePath, dependencies = {}) {
165
+ if (dependencies.containmentRoot !== undefined) {
166
+ await rejectLinkedComponents(dependencies.containmentRoot, filePath);
167
+ }
168
+ let plannedIdentity;
6
169
  try {
7
- const info = await lstat(filePath);
8
- if (info.isSymbolicLink())
9
- throw new KiokukoError('SECURITY_REJECTION', `Refusing to follow symlink: ${filePath}`);
170
+ const planned = await lstat(filePath, { bigint: true });
171
+ if (planned.isSymbolicLink()) {
172
+ throw new KiokukoError('SECURITY_REJECTION', 'Refusing to follow a symbolic link');
173
+ }
174
+ if (!planned.isFile())
175
+ throw new KiokukoError('VALIDATION_ERROR', `Expected a regular file: ${filePath}`);
176
+ plannedIdentity = { device: planned.dev, inode: planned.ino };
177
+ if (plannedIdentity.inode === 0n) {
178
+ throw new KiokukoError('SECURITY_REJECTION', 'The filesystem does not expose a stable file identity required for safe mutation');
179
+ }
180
+ }
181
+ catch (error) {
182
+ if (isMissingFile(error))
183
+ return undefined;
184
+ throw error;
185
+ }
186
+ let handle;
187
+ const platform = dependencies.platform ?? process.platform;
188
+ const flags = platform === 'win32'
189
+ ? constants.O_RDONLY
190
+ : constants.O_RDONLY | constants.O_NOFOLLOW;
191
+ try {
192
+ handle = await open(filePath, flags);
193
+ }
194
+ catch (error) {
195
+ if (isMissingFile(error))
196
+ throw changedAfterPlanning();
197
+ if (isSymlinkFailure(error))
198
+ throw new KiokukoError('SECURITY_REJECTION', 'Refusing to follow a symbolic link');
199
+ throw error;
200
+ }
201
+ let operationResult;
202
+ let operationFailed = false;
203
+ let operationError;
204
+ try {
205
+ await dependencies.afterOpen?.(filePath, handle);
206
+ const info = await handle.stat();
10
207
  if (!info.isFile())
11
208
  throw new KiokukoError('VALIDATION_ERROR', `Expected a regular file: ${filePath}`);
12
- return { content: await readFile(filePath, 'utf8'), mode: info.mode & 0o777 };
209
+ const identity = identityFromStat(info);
210
+ if (!sameIdentity(identity, plannedIdentity))
211
+ throw changedAfterPlanning();
212
+ const content = decodeUtf8(await handle.readFile(), filePath);
213
+ operationResult = { value: { content, mode: info.mode & 0o777, identity } };
214
+ }
215
+ catch (error) {
216
+ operationFailed = true;
217
+ operationError = error;
218
+ }
219
+ try {
220
+ await (dependencies.closeHandle ?? ((openHandle) => openHandle.close()))(handle);
221
+ }
222
+ catch (closeError) {
223
+ if (operationFailed) {
224
+ throw new AggregateError([operationError, closeError], 'Regular-file read failed and its file descriptor could not be closed');
225
+ }
226
+ throw closeError;
227
+ }
228
+ if (operationFailed)
229
+ throw operationError;
230
+ if (operationResult === undefined) {
231
+ throw new KiokukoError('INTEGRITY_ERROR', 'Regular-file read produced no result');
232
+ }
233
+ return operationResult.value;
234
+ }
235
+ async function readBoundArtifact(filePath, containmentRoot) {
236
+ const snapshot = await readRegularFile(filePath, {
237
+ ...(containmentRoot === undefined ? {} : { containmentRoot }),
238
+ });
239
+ if (snapshot === undefined)
240
+ return undefined;
241
+ let current;
242
+ try {
243
+ current = await lstat(filePath, { bigint: true });
13
244
  }
14
245
  catch (error) {
15
- if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
246
+ if (isMissingFile(error))
16
247
  return undefined;
17
248
  throw error;
18
249
  }
250
+ if (current.isSymbolicLink()) {
251
+ throw new KiokukoError('SECURITY_REJECTION', 'Refusing to follow a symbolic link');
252
+ }
253
+ if (!current.isFile()) {
254
+ throw new KiokukoError('VALIDATION_ERROR', `Expected a regular file: ${filePath}`);
255
+ }
256
+ const identity = { device: current.dev, inode: current.ino };
257
+ if (identity.inode === 0n) {
258
+ throw new KiokukoError('SECURITY_REJECTION', 'The filesystem does not expose a stable file identity required for safe mutation');
259
+ }
260
+ if (!sameIdentity(identity, snapshot.identity) || Number(current.mode & 511n) !== snapshot.mode) {
261
+ throw changedAfterPlanning(filePath);
262
+ }
263
+ return { snapshot, linkCount: current.nlink };
264
+ }
265
+ /** Read an exact directory identity without following its final path component. */
266
+ export async function readDirectoryIdentity(directoryPath) {
267
+ let info;
268
+ try {
269
+ info = await lstat(directoryPath, { bigint: true });
270
+ }
271
+ catch (error) {
272
+ if (isMissingFile(error))
273
+ return undefined;
274
+ throw error;
275
+ }
276
+ if (info.isSymbolicLink()) {
277
+ throw new KiokukoError('SECURITY_REJECTION', 'Refusing to traverse a symbolic link');
278
+ }
279
+ if (!info.isDirectory()) {
280
+ throw new KiokukoError('VALIDATION_ERROR', `Expected a directory: ${directoryPath}`);
281
+ }
282
+ if (info.ino === 0n) {
283
+ throw new KiokukoError('SECURITY_REJECTION', 'The filesystem does not expose a stable directory identity required for safe mutation');
284
+ }
285
+ return { device: info.dev, inode: info.ino };
286
+ }
287
+ const ATOMIC_QUARANTINE_NAME = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.(deleted|previous|rollback)$/iu;
288
+ const ATOMIC_QUARANTINE_CLEANUP_NAME = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.(deleted|previous|rollback)\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.cleanup$/iu;
289
+ /**
290
+ * Bind a native atomic-write quarantine for this exact basename without
291
+ * following path components. Its snapshot may constrain convergence, but it
292
+ * never grants the caller ownership of the artifact.
293
+ */
294
+ export async function readPendingAtomicMutation(filePath, expectedParentDirectory, containmentRoot) {
295
+ if (containmentRoot !== undefined) {
296
+ await rejectLinkedComponents(containmentRoot, filePath);
297
+ }
298
+ const directory = path.dirname(filePath);
299
+ const parent = await bindMutationDirectory(directory, expectedParentDirectory);
300
+ const basename = path.basename(filePath);
301
+ const activePrefix = `.${basename}.`;
302
+ const cleanupPrefix = `..${basename}.`;
303
+ const names = await readdir(directory);
304
+ await requireBoundDirectory(parent);
305
+ if (containmentRoot !== undefined) {
306
+ await rejectLinkedComponents(containmentRoot, filePath);
307
+ }
308
+ const matches = names.flatMap((name) => {
309
+ const match = name.startsWith(activePrefix)
310
+ ? ATOMIC_QUARANTINE_NAME.exec(name.slice(activePrefix.length))
311
+ : name.startsWith(cleanupPrefix)
312
+ ? ATOMIC_QUARANTINE_CLEANUP_NAME.exec(name.slice(cleanupPrefix.length))
313
+ : null;
314
+ if (match === null)
315
+ return [];
316
+ const kind = match[1];
317
+ if (kind !== 'deleted' && kind !== 'previous' && kind !== 'rollback')
318
+ return [];
319
+ return [{ name, kind }];
320
+ });
321
+ if (matches.length === 0)
322
+ return undefined;
323
+ if (matches.length !== 1) {
324
+ throw new KiokukoError('CONFLICT', 'Multiple atomic quarantines exist for one target', { target: filePath });
325
+ }
326
+ const match = matches[0];
327
+ if (match === undefined)
328
+ throw new KiokukoError('INTEGRITY_ERROR', 'Atomic quarantine match vanished');
329
+ const artifactPath = path.join(directory, match.name);
330
+ const artifact = await readArtifactInBoundDirectory(artifactPath, parent, containmentRoot);
331
+ if (artifact === undefined)
332
+ throw changedAfterPlanning(artifactPath);
333
+ if (artifact.linkCount !== 1n) {
334
+ throw new KiokukoError('SECURITY_REJECTION', 'Atomic quarantine must have exactly one link', { target: artifactPath });
335
+ }
336
+ await requireBoundDirectory(parent);
337
+ return { kind: match.kind, snapshot: artifact.snapshot };
338
+ }
339
+ async function readBoundDirectory(directoryPath) {
340
+ const identity = await readDirectoryIdentity(directoryPath);
341
+ if (identity === undefined)
342
+ throw changedAfterPlanning(directoryPath);
343
+ return {
344
+ directoryPath,
345
+ identity,
346
+ };
347
+ }
348
+ async function requireBoundDirectory(binding) {
349
+ const current = await readBoundDirectory(binding.directoryPath);
350
+ if (!sameIdentity(current.identity, binding.identity)) {
351
+ throw changedAfterPlanning(binding.directoryPath);
352
+ }
353
+ }
354
+ async function readArtifactInBoundDirectory(filePath, parent, containmentRoot) {
355
+ await requireBoundDirectory(parent);
356
+ const artifact = await readBoundArtifact(filePath, containmentRoot);
357
+ await requireBoundDirectory(parent);
358
+ return artifact;
359
+ }
360
+ async function requireAbsentInBoundDirectory(filePath, parent, message, containmentRoot) {
361
+ if (await readArtifactInBoundDirectory(filePath, parent, containmentRoot) !== undefined) {
362
+ throw new KiokukoError('INTEGRITY_ERROR', message, { target: filePath });
363
+ }
364
+ }
365
+ async function bindMutationDirectory(directoryPath, expected) {
366
+ const binding = await readBoundDirectory(directoryPath);
367
+ if (expected !== undefined && !sameIdentity(binding.identity, expected)) {
368
+ throw changedAfterPlanning(directoryPath);
369
+ }
370
+ return binding;
371
+ }
372
+ export async function assertFileExpectation(filePath, expectation) {
373
+ const parent = expectation.expectedParentDirectory === undefined
374
+ ? undefined
375
+ : await bindMutationDirectory(path.dirname(filePath), expectation.expectedParentDirectory);
376
+ const current = await readRegularFile(filePath, {
377
+ ...(expectation.containmentRoot === undefined ? {} : { containmentRoot: expectation.containmentRoot }),
378
+ });
379
+ if (!sameSnapshot(current, expectation.expected))
380
+ throw changedAfterPlanning(filePath);
381
+ if (parent !== undefined)
382
+ await requireBoundDirectory(parent);
383
+ for (const absentPath of expectation.mustRemainAbsent ?? []) {
384
+ const alternate = await readRegularFile(absentPath, {
385
+ ...(expectation.containmentRoot === undefined ? {} : { containmentRoot: expectation.containmentRoot }),
386
+ });
387
+ if (alternate !== undefined)
388
+ throw changedAfterPlanning(absentPath);
389
+ if (parent !== undefined)
390
+ await requireBoundDirectory(parent);
391
+ }
392
+ }
393
+ async function cleanupBoundArtifact(beforeCleanup, filePath, expected, parent) {
394
+ if (expected === undefined) {
395
+ return new AtomicCleanupFailure(undefined, {
396
+ cause: new KiokukoError('INTEGRITY_ERROR', 'Refusing to remove an atomic artifact whose exact identity was not bound'),
397
+ });
398
+ }
399
+ const disposalPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.cleanup`);
400
+ try {
401
+ requireArtifact(await readArtifactInBoundDirectory(filePath, parent), expected, 'Refusing to remove a changed atomic artifact');
402
+ await requireAbsentInBoundDirectory(disposalPath, parent, 'Atomic cleanup quarantine already exists');
403
+ await rename(filePath, disposalPath);
404
+ requireArtifact(await readArtifactInBoundDirectory(disposalPath, parent), expected, 'Atomic cleanup quarantine does not match the bound artifact');
405
+ await requireAbsentInBoundDirectory(filePath, parent, 'Atomic cleanup source pathname was replaced');
406
+ await beforeCleanup?.(disposalPath);
407
+ requireArtifact(await readArtifactInBoundDirectory(disposalPath, parent), expected, 'Atomic cleanup quarantine changed during the cleanup hook');
408
+ await requireAbsentInBoundDirectory(filePath, parent, 'Atomic cleanup hook recreated the source pathname');
409
+ await unlink(disposalPath);
410
+ await requireAbsentInBoundDirectory(disposalPath, parent, 'Atomic cleanup quarantine was not removed');
411
+ return undefined;
412
+ }
413
+ catch (error) {
414
+ return bindOwnedCleanupFailure(error, [
415
+ { artifactPath: disposalPath, expected },
416
+ { artifactPath: filePath, expected },
417
+ ], parent);
418
+ }
419
+ }
420
+ /**
421
+ * Report a recovery pathname only after rebinding the exact inode, bytes, and
422
+ * mode. A changed link count is itself a cleanup failure, but it does not make
423
+ * the pathname's inode ownership ambiguous.
424
+ */
425
+ async function bindOwnedCleanupFailure(cause, candidates, parent) {
426
+ const observationErrors = [];
427
+ for (const candidate of candidates) {
428
+ try {
429
+ await requireBoundDirectory(parent);
430
+ const current = await readBoundArtifact(candidate.artifactPath);
431
+ await requireBoundDirectory(parent);
432
+ if (current !== undefined
433
+ && sameSnapshot(current.snapshot, candidate.expected.snapshot)) {
434
+ return new AtomicCleanupFailure(candidate.artifactPath, { cause });
435
+ }
436
+ }
437
+ catch (observationError) {
438
+ observationErrors.push(observationError);
439
+ }
440
+ }
441
+ return new AtomicCleanupFailure(undefined, {
442
+ cause: observationErrors.length === 0
443
+ ? cause
444
+ : new AggregateError([cause, ...observationErrors], 'Atomic cleanup failed and no owned artifact pathname could be proven'),
445
+ });
446
+ }
447
+ async function cleanupBeforeCommit(beforeCleanup, filePath, operationError, expected, parent) {
448
+ const cleanupFailure = await cleanupBoundArtifact(beforeCleanup, filePath, expected, parent);
449
+ if (cleanupFailure !== undefined) {
450
+ throw new AggregateError([operationError, cleanupFailure], 'Atomic write failed and temporary-file cleanup also failed');
451
+ }
452
+ if (operationError instanceof KiokukoError || operationError instanceof AggregateError)
453
+ throw operationError;
454
+ throw new KiokukoError('PARTIAL_FAILURE', `Unable to atomically write ${filePath}`, {
455
+ cause: operationError instanceof Error ? operationError.message : String(operationError),
456
+ });
457
+ }
458
+ async function restoreQuarantinedTarget(quarantinePath, filePath, beforeCleanup, quarantined, parent) {
459
+ try {
460
+ requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent), quarantined, 'Quarantined target changed before rollback');
461
+ await requireAbsentInBoundDirectory(filePath, parent, 'Rollback target pathname is no longer absent');
462
+ await link(quarantinePath, filePath);
463
+ const linked = withLinkCount(quarantined, quarantined.linkCount + 1n);
464
+ requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent), linked, 'Quarantined target changed while rollback was installed');
465
+ requireArtifact(await readArtifactInBoundDirectory(filePath, parent), linked, 'Restored target does not match the bound quarantine');
466
+ const cleanupFailure = await cleanupBoundArtifact(beforeCleanup, quarantinePath, linked, parent);
467
+ if (cleanupFailure !== undefined)
468
+ return [cleanupFailure];
469
+ requireArtifact(await readArtifactInBoundDirectory(filePath, parent), quarantined, 'Restored target changed during quarantine cleanup');
470
+ return [];
471
+ }
472
+ catch (error) {
473
+ return [await bindOwnedCleanupFailure(error, [
474
+ { artifactPath: quarantinePath, expected: quarantined },
475
+ { artifactPath: quarantinePath, expected: withLinkCount(quarantined, quarantined.linkCount + 1n) },
476
+ ], parent)];
477
+ }
478
+ }
479
+ function throwWithArtifactFailures(error, failures, message) {
480
+ if (failures.length === 0)
481
+ throw error;
482
+ throw new AggregateError([error, ...failures], message);
483
+ }
484
+ async function throwAfterOriginalRestoreAttempt(operationError, failures, filePath, expected, parent, message) {
485
+ let current;
486
+ try {
487
+ await requireBoundDirectory(parent);
488
+ current = await readRegularFile(filePath);
489
+ await requireBoundDirectory(parent);
490
+ }
491
+ catch (validationError) {
492
+ throw new AtomicCommittedUnlinkError({
493
+ cleanupFailures: [
494
+ ...failures,
495
+ new AtomicCleanupFailure(undefined, { cause: validationError }),
496
+ ],
497
+ }, operationError);
498
+ }
499
+ if (sameSnapshot(current, expected)) {
500
+ if (failures.length === 0)
501
+ throw operationError;
502
+ throw new AggregateError([operationError, ...failures], message);
503
+ }
504
+ throw new AtomicCommittedUnlinkError({ cleanupFailures: failures }, operationError);
505
+ }
506
+ async function conditionalUnlinkInstalled(temporaryPath, filePath, expected, beforeCleanup, dependencies, parent, state) {
507
+ requireArtifact(await readArtifactInBoundDirectory(filePath, parent), expected, 'Installed target changed before conditional rollback');
508
+ const quarantinePath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.rollback`);
509
+ await requireAbsentInBoundDirectory(quarantinePath, parent, 'Conditional rollback quarantine already exists');
510
+ await dependencies.beforeRename?.(filePath, quarantinePath);
511
+ requireArtifact(await readArtifactInBoundDirectory(filePath, parent), expected, 'Installed target changed during the conditional rollback hook');
512
+ await requireAbsentInBoundDirectory(quarantinePath, parent, 'Conditional rollback hook created its quarantine path');
513
+ await rename(filePath, quarantinePath);
514
+ // Built-in rename fulfillment is the commit boundary. Every later failure
515
+ // reports an absent-target committed outcome.
516
+ let postRenameError;
517
+ try {
518
+ await dependencies.afterRename?.(filePath, quarantinePath);
519
+ }
520
+ catch (error) {
521
+ postRenameError = error;
522
+ }
523
+ try {
524
+ requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent), expected, 'Installed target changed while entering conditional rollback');
525
+ await requireAbsentInBoundDirectory(filePath, parent, 'Conditional rollback source pathname was recreated');
526
+ }
527
+ catch (error) {
528
+ const cleanupFailures = [await bindOwnedCleanupFailure(error, [
529
+ { artifactPath: quarantinePath, expected },
530
+ ], parent)];
531
+ try {
532
+ const currentTemporary = await readArtifactInBoundDirectory(temporaryPath, parent);
533
+ if (currentTemporary !== undefined
534
+ && sameSnapshot(currentTemporary.snapshot, expected.snapshot)) {
535
+ state.temporaryLinkCount = currentTemporary.linkCount;
536
+ }
537
+ }
538
+ catch (observationError) {
539
+ cleanupFailures.push(new AtomicCleanupFailure(undefined, { cause: observationError }));
540
+ }
541
+ throw new AtomicCommittedUnlinkError({
542
+ cleanupFailures,
543
+ }, postRenameError ?? error);
544
+ }
545
+ const cleanupFailure = await cleanupBoundArtifact(beforeCleanup, quarantinePath, expected, parent);
546
+ const cleanupFailures = cleanupFailure === undefined ? [] : [cleanupFailure];
547
+ if (cleanupFailure === undefined) {
548
+ state.temporaryLinkCount = 1n;
549
+ }
550
+ else {
551
+ try {
552
+ const currentTemporary = await readArtifactInBoundDirectory(temporaryPath, parent);
553
+ if (currentTemporary !== undefined
554
+ && sameSnapshot(currentTemporary.snapshot, expected.snapshot)) {
555
+ state.temporaryLinkCount = currentTemporary.linkCount;
556
+ }
557
+ }
558
+ catch (observationError) {
559
+ cleanupFailures.push(new AtomicCleanupFailure(undefined, { cause: observationError }));
560
+ }
561
+ }
562
+ const outcome = { cleanupFailures };
563
+ try {
564
+ await requireBoundDirectory(parent);
565
+ }
566
+ catch (error) {
567
+ throw new AtomicCommittedUnlinkError(outcome, error);
568
+ }
569
+ if (postRenameError !== undefined) {
570
+ throw new AtomicCommittedUnlinkError(outcome, postRenameError);
571
+ }
572
+ return outcome;
19
573
  }
20
- export async function atomicWriteText(filePath, content, mode = 0o644) {
574
+ async function conditionalInstall(temporaryPath, filePath, expectation, beforeCleanup, temporary, parent, state, dependencies) {
575
+ await requireBoundDirectory(parent);
576
+ await assertFileExpectation(filePath, expectation);
577
+ const expected = expectation.expected;
578
+ let quarantinePath;
579
+ let quarantined;
580
+ if (expected !== undefined) {
581
+ const plannedTarget = await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot);
582
+ if (plannedTarget === undefined || !sameSnapshot(plannedTarget.snapshot, expected)) {
583
+ throw changedAfterPlanning(filePath);
584
+ }
585
+ quarantinePath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.previous`);
586
+ await requireAbsentInBoundDirectory(quarantinePath, parent, 'Conditional update quarantine already exists', expectation.containmentRoot);
587
+ await dependencies.beforeRename?.(filePath, quarantinePath);
588
+ requireArtifact(await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot), plannedTarget, 'Planned target changed during the update quarantine hook');
589
+ await requireAbsentInBoundDirectory(quarantinePath, parent, 'Conditional update hook created its quarantine path', expectation.containmentRoot);
590
+ await rename(filePath, quarantinePath);
591
+ // Built-in rename fulfillment is the commit boundary. Retain the exact
592
+ // pre-rename binding before any post-rename hook or pathname observation.
593
+ quarantined = plannedTarget;
594
+ let postRenameError;
595
+ try {
596
+ await dependencies.afterRename?.(filePath, quarantinePath);
597
+ }
598
+ catch (error) {
599
+ postRenameError = error;
600
+ }
601
+ try {
602
+ requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent, expectation.containmentRoot), quarantined, 'Quarantined target changed after update rename');
603
+ await requireAbsentInBoundDirectory(filePath, parent, 'Update quarantine left or recreated the target pathname', expectation.containmentRoot);
604
+ }
605
+ catch (error) {
606
+ throw new AtomicCommittedUnlinkError({
607
+ cleanupFailures: [await bindOwnedCleanupFailure(error, [
608
+ { artifactPath: quarantinePath, expected: quarantined },
609
+ ], parent)],
610
+ }, postRenameError ?? error);
611
+ }
612
+ if (postRenameError !== undefined) {
613
+ throw new AtomicCommittedUnlinkError({
614
+ cleanupFailures: [await bindOwnedCleanupFailure(postRenameError, [
615
+ { artifactPath: quarantinePath, expected: quarantined },
616
+ ], parent)],
617
+ }, postRenameError);
618
+ }
619
+ try {
620
+ await dependencies.afterQuarantine?.(filePath);
621
+ requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent, expectation.containmentRoot), quarantined, 'Quarantined target changed after quarantine');
622
+ await requireAbsentInBoundDirectory(filePath, parent, 'Quarantine hook recreated the target pathname', expectation.containmentRoot);
623
+ }
624
+ catch (error) {
625
+ const failures = await restoreQuarantinedTarget(quarantinePath, filePath, beforeCleanup, quarantined, parent);
626
+ await throwAfterOriginalRestoreAttempt(error, failures, filePath, expected, parent, 'Conditional installation was interrupted and rollback left an artifact');
627
+ }
628
+ }
629
+ const validatePreInstall = async () => {
630
+ requireArtifact(await readArtifactInBoundDirectory(temporaryPath, parent, expectation.containmentRoot), temporary, 'Atomic temporary file changed before installation');
631
+ if (expected === undefined) {
632
+ if (await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot) !== undefined) {
633
+ throw changedAfterPlanning(filePath);
634
+ }
635
+ }
636
+ else {
637
+ await requireAbsentInBoundDirectory(filePath, parent, 'Conditional installation target is no longer absent', expectation.containmentRoot);
638
+ }
639
+ if (quarantinePath !== undefined && quarantined !== undefined) {
640
+ requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent, expectation.containmentRoot), quarantined, 'Quarantined target changed before installation');
641
+ }
642
+ };
643
+ try {
644
+ await validatePreInstall();
645
+ await dependencies.beforeLink?.(temporaryPath, filePath);
646
+ await validatePreInstall();
647
+ try {
648
+ await link(temporaryPath, filePath);
649
+ }
650
+ catch (error) {
651
+ if (isAlreadyExists(error))
652
+ throw changedAfterPlanning(filePath);
653
+ throw error;
654
+ }
655
+ }
656
+ catch (error) {
657
+ const failures = quarantinePath === undefined || quarantined === undefined
658
+ ? []
659
+ : await restoreQuarantinedTarget(quarantinePath, filePath, beforeCleanup, quarantined, parent);
660
+ if (quarantined !== undefined && expected !== undefined) {
661
+ await throwAfterOriginalRestoreAttempt(error, failures, filePath, expected, parent, 'Conditional installation failed and rollback left an artifact');
662
+ }
663
+ throwWithArtifactFailures(error, failures, 'Conditional installation failed and rollback left an artifact');
664
+ }
665
+ // Built-in link fulfillment is the commit boundary. Record the second link
666
+ // before any hook or pathname validation can fail.
667
+ state.temporaryLinkCount = 2n;
668
+ const linkedTemporary = withLinkCount(temporary, 2n);
669
+ const validateInstalled = async () => {
670
+ requireArtifact(await readArtifactInBoundDirectory(temporaryPath, parent, expectation.containmentRoot), linkedTemporary, 'Atomic temporary file changed after installation');
671
+ requireArtifact(await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot), linkedTemporary, 'Installed target changed after installation');
672
+ if (quarantinePath !== undefined && quarantined !== undefined) {
673
+ requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent, expectation.containmentRoot), quarantined, 'Quarantined target changed after installation');
674
+ }
675
+ };
676
+ try {
677
+ await dependencies.afterLink?.(temporaryPath, filePath);
678
+ await validateInstalled();
679
+ await dependencies.afterInstall?.(filePath);
680
+ await validateInstalled();
681
+ }
682
+ catch (error) {
683
+ const cleanupFailures = quarantinePath === undefined || quarantined === undefined
684
+ ? []
685
+ : [await bindOwnedCleanupFailure(error, [
686
+ { artifactPath: quarantinePath, expected: quarantined },
687
+ ], parent)];
688
+ throw new AtomicCommittedMutationError({ installed: temporary.snapshot, cleanupFailures }, error);
689
+ }
690
+ for (const absentPath of expectation.mustRemainAbsent ?? []) {
691
+ let alternate;
692
+ try {
693
+ await requireBoundDirectory(parent);
694
+ alternate = await readRegularFile(absentPath, {
695
+ ...(expectation.containmentRoot === undefined ? {} : { containmentRoot: expectation.containmentRoot }),
696
+ });
697
+ await requireBoundDirectory(parent);
698
+ }
699
+ catch (error) {
700
+ const displacedOriginal = quarantinePath === undefined || quarantined === undefined
701
+ ? []
702
+ : [await bindOwnedCleanupFailure(error, [
703
+ { artifactPath: quarantinePath, expected: quarantined },
704
+ ], parent)];
705
+ throw new AtomicCommittedMutationError({ installed: temporary.snapshot, cleanupFailures: displacedOriginal }, error);
706
+ }
707
+ if (alternate !== undefined) {
708
+ const conflict = changedAfterPlanning(absentPath);
709
+ let rollback;
710
+ try {
711
+ rollback = await conditionalUnlinkInstalled(temporaryPath, filePath, linkedTemporary, beforeCleanup, dependencies, parent, state);
712
+ }
713
+ catch (error) {
714
+ const rollbackFailure = new AggregateError([conflict, error], 'Alternate target appeared and conditional rollback failed');
715
+ if (error instanceof AtomicCommittedUnlinkError) {
716
+ const failures = [...error.outcome.cleanupFailures];
717
+ if (quarantinePath !== undefined
718
+ && quarantined !== undefined
719
+ && expected !== undefined) {
720
+ failures.push(...await restoreQuarantinedTarget(quarantinePath, filePath, beforeCleanup, quarantined, parent));
721
+ await throwAfterOriginalRestoreAttempt(rollbackFailure, failures, filePath, expected, parent, 'Alternate target appeared; original target was restored but artifact cleanup failed');
722
+ }
723
+ throwWithArtifactFailures(rollbackFailure, failures, 'Alternate target appeared and committed rollback left an artifact');
724
+ }
725
+ const displacedOriginal = quarantinePath === undefined || quarantined === undefined
726
+ ? []
727
+ : [await bindOwnedCleanupFailure(error, [
728
+ { artifactPath: quarantinePath, expected: quarantined },
729
+ ], parent)];
730
+ throw new AtomicCommittedMutationError({
731
+ installed: temporary.snapshot,
732
+ cleanupFailures: displacedOriginal,
733
+ }, rollbackFailure);
734
+ }
735
+ const failures = [...rollback.cleanupFailures];
736
+ if (quarantinePath !== undefined
737
+ && quarantined !== undefined
738
+ && expected !== undefined) {
739
+ failures.push(...await restoreQuarantinedTarget(quarantinePath, filePath, beforeCleanup, quarantined, parent));
740
+ await throwAfterOriginalRestoreAttempt(conflict, failures, filePath, expected, parent, 'Alternate target appeared; original target was restored but artifact cleanup failed');
741
+ }
742
+ throwWithArtifactFailures(conflict, failures, 'Alternate target appeared and committed rollback left an artifact');
743
+ }
744
+ }
745
+ const cleanupFailures = [];
746
+ if (quarantinePath !== undefined && quarantined !== undefined) {
747
+ const cleanupFailure = await cleanupBoundArtifact(beforeCleanup, quarantinePath, quarantined, parent);
748
+ if (cleanupFailure !== undefined)
749
+ cleanupFailures.push(cleanupFailure);
750
+ }
751
+ try {
752
+ await requireBoundDirectory(parent);
753
+ }
754
+ catch (error) {
755
+ throw new AtomicCommittedMutationError({ installed: temporary.snapshot, cleanupFailures }, error);
756
+ }
757
+ return { installed: temporary.snapshot, cleanupFailures };
758
+ }
759
+ async function atomicWriteTextInternal(filePath, content, mode, dependencyOverrides, expectation) {
760
+ const changeMode = dependencyOverrides.chmod ?? chmod;
761
+ const beforeCleanup = dependencyOverrides.beforeCleanup;
21
762
  const directory = path.dirname(filePath);
22
- await mkdir(directory, { recursive: true });
763
+ if (expectation?.containmentRoot !== undefined) {
764
+ await rejectLinkedComponents(expectation.containmentRoot, filePath);
765
+ }
766
+ if (expectation?.expectedParentDirectory === undefined) {
767
+ await mkdir(directory, { recursive: true });
768
+ }
769
+ else {
770
+ await assertFileExpectation(filePath, expectation);
771
+ }
772
+ const parent = await bindMutationDirectory(directory, expectation?.expectedParentDirectory);
773
+ if (expectation?.containmentRoot !== undefined) {
774
+ await rejectLinkedComponents(expectation.containmentRoot, filePath);
775
+ }
23
776
  const temporaryPath = path.join(directory, `.${path.basename(filePath)}.${randomUUID()}.tmp`);
777
+ let temporaryHandle;
778
+ let temporaryCreated = false;
779
+ let temporary;
24
780
  try {
25
- await writeFile(temporaryPath, content, { encoding: 'utf8', mode });
26
- await chmod(temporaryPath, mode);
27
- await rename(temporaryPath, filePath);
781
+ temporaryHandle = await open(temporaryPath, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL, mode);
782
+ temporaryCreated = true;
783
+ await requireBoundDirectory(parent);
784
+ const opened = artifactFromOpenHandle(await temporaryHandle.stat(), '');
785
+ if (opened.linkCount !== 1n) {
786
+ throw new KiokukoError('INTEGRITY_ERROR', 'Atomic temporary file must have exactly one link');
787
+ }
788
+ await temporaryHandle.writeFile(content, { encoding: 'utf8' });
789
+ await temporaryHandle.sync();
790
+ const written = await readOpenHandleArtifact(temporaryHandle, temporaryPath);
791
+ if (!sameIdentity(written.snapshot.identity, opened.snapshot.identity)
792
+ || written.snapshot.content !== content
793
+ || written.linkCount !== 1n) {
794
+ throw new KiokukoError('INTEGRITY_ERROR', 'Atomic temporary file changed during preparation');
795
+ }
796
+ await requireBoundDirectory(parent);
797
+ await changeMode(temporaryPath, mode);
798
+ await temporaryHandle.sync();
799
+ const prepared = await readOpenHandleArtifact(temporaryHandle, temporaryPath);
800
+ if (!sameIdentity(prepared.snapshot.identity, opened.snapshot.identity)
801
+ || prepared.snapshot.content !== content
802
+ || prepared.snapshot.mode !== (mode & 0o777)
803
+ || prepared.linkCount !== 1n) {
804
+ throw new KiokukoError('INTEGRITY_ERROR', 'Atomic temporary file changed during mode preparation');
805
+ }
806
+ await requireBoundDirectory(parent);
807
+ requireArtifact(await readBoundArtifact(temporaryPath, expectation?.containmentRoot), prepared, 'Atomic temporary pathname changed during mode preparation');
808
+ await dependencyOverrides.beforeCommit?.(filePath);
809
+ await requireBoundDirectory(parent);
810
+ const descriptorPrepared = await readOpenHandleArtifact(temporaryHandle, temporaryPath);
811
+ requireArtifact(descriptorPrepared, prepared, 'Atomic temporary file changed before commit');
812
+ temporary = requireArtifact(await readBoundArtifact(temporaryPath, expectation?.containmentRoot), prepared, 'Atomic temporary pathname changed before commit');
813
+ await temporaryHandle.close();
814
+ temporaryHandle = undefined;
28
815
  }
29
816
  catch (error) {
30
- await unlink(temporaryPath).catch(() => undefined);
817
+ let operationError = error;
818
+ let cleanupBinding;
819
+ if (temporaryHandle !== undefined) {
820
+ try {
821
+ cleanupBinding = await readOpenHandleArtifact(temporaryHandle, temporaryPath);
822
+ }
823
+ catch (bindingError) {
824
+ operationError = new AggregateError([operationError, bindingError], 'Atomic write failed and its temporary artifact could not be rebound');
825
+ }
826
+ try {
827
+ await temporaryHandle.close();
828
+ }
829
+ catch (closeError) {
830
+ operationError = new AggregateError([operationError, closeError], 'Atomic write failed and its temporary handle could not be closed');
831
+ }
832
+ temporaryHandle = undefined;
833
+ }
834
+ if (temporaryCreated) {
835
+ return cleanupBeforeCommit(beforeCleanup, temporaryPath, operationError, cleanupBinding, parent);
836
+ }
837
+ if (operationError instanceof KiokukoError || operationError instanceof AggregateError)
838
+ throw operationError;
31
839
  throw new KiokukoError('PARTIAL_FAILURE', `Unable to atomically write ${filePath}`, {
32
- cause: error instanceof Error ? error.message : String(error),
840
+ cause: operationError instanceof Error ? operationError.message : String(operationError),
33
841
  });
34
842
  }
843
+ if (expectation === undefined) {
844
+ try {
845
+ requireArtifact(await readArtifactInBoundDirectory(temporaryPath, parent), temporary, 'Atomic temporary file changed before rename');
846
+ await dependencyOverrides.beforeRename?.(temporaryPath, filePath);
847
+ requireArtifact(await readArtifactInBoundDirectory(temporaryPath, parent), temporary, 'Atomic temporary file changed during the rename hook');
848
+ await rename(temporaryPath, filePath);
849
+ }
850
+ catch (error) {
851
+ return cleanupBeforeCommit(beforeCleanup, temporaryPath, error, temporary, parent);
852
+ }
853
+ // Built-in rename fulfillment is the commit boundary. Construct the
854
+ // committed outcome before any post-rename hook or observation.
855
+ const outcome = { installed: temporary.snapshot, cleanupFailures: [] };
856
+ try {
857
+ await dependencyOverrides.afterRename?.(temporaryPath, filePath);
858
+ requireArtifact(await readArtifactInBoundDirectory(filePath, parent), temporary, 'Renamed target does not match the bound temporary file');
859
+ await requireAbsentInBoundDirectory(temporaryPath, parent, 'Unconditional rename left or recreated its temporary pathname');
860
+ await dependencyOverrides.afterInstall?.(filePath);
861
+ requireArtifact(await readArtifactInBoundDirectory(filePath, parent), temporary, 'Installed target changed after rename');
862
+ await requireAbsentInBoundDirectory(temporaryPath, parent, 'Post-install hook recreated the temporary pathname');
863
+ return outcome;
864
+ }
865
+ catch (error) {
866
+ const cleanupFailures = [];
867
+ try {
868
+ const installed = await readArtifactInBoundDirectory(filePath, parent);
869
+ const temporaryAlias = await readArtifactInBoundDirectory(temporaryPath, parent);
870
+ if (!sameArtifact(installed, temporary)) {
871
+ cleanupFailures.push(new AtomicCleanupFailure(temporaryAlias !== undefined
872
+ && sameSnapshot(temporaryAlias.snapshot, temporary.snapshot)
873
+ ? temporaryPath
874
+ : undefined, { cause: error }));
875
+ }
876
+ }
877
+ catch (observationError) {
878
+ cleanupFailures.push(new AtomicCleanupFailure(undefined, { cause: observationError }));
879
+ }
880
+ throw new AtomicCommittedMutationError({
881
+ installed: temporary.snapshot,
882
+ cleanupFailures,
883
+ }, error);
884
+ }
885
+ }
886
+ const state = { temporaryLinkCount: 1n };
887
+ let outcome;
888
+ try {
889
+ outcome = await conditionalInstall(temporaryPath, filePath, expectation, beforeCleanup, temporary, parent, state, dependencyOverrides);
890
+ }
891
+ catch (error) {
892
+ const cleanupFailure = await cleanupBoundArtifact(beforeCleanup, temporaryPath, withLinkCount(temporary, state.temporaryLinkCount), parent);
893
+ if (error instanceof AtomicCommittedUnlinkError) {
894
+ throw new AtomicCommittedUnlinkError({
895
+ cleanupFailures: cleanupFailure === undefined
896
+ ? error.outcome.cleanupFailures
897
+ : [...error.outcome.cleanupFailures, cleanupFailure],
898
+ }, error.operationError);
899
+ }
900
+ if (error instanceof AtomicCommittedMutationError) {
901
+ throw new AtomicCommittedMutationError({
902
+ installed: error.outcome.installed,
903
+ cleanupFailures: cleanupFailure === undefined
904
+ ? error.outcome.cleanupFailures
905
+ : [...error.outcome.cleanupFailures, cleanupFailure],
906
+ }, error.operationError);
907
+ }
908
+ if (cleanupFailure !== undefined) {
909
+ throw new AggregateError([error, cleanupFailure], 'Atomic file mutation failed and temporary artifact cleanup also failed');
910
+ }
911
+ throw error;
912
+ }
913
+ const cleanupFailure = await cleanupBoundArtifact(beforeCleanup, temporaryPath, withLinkCount(temporary, state.temporaryLinkCount), parent);
914
+ const finalOutcome = cleanupFailure === undefined
915
+ ? outcome
916
+ : { installed: outcome.installed, cleanupFailures: [...outcome.cleanupFailures, cleanupFailure] };
917
+ try {
918
+ if (cleanupFailure === undefined) {
919
+ requireArtifact(await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot), temporary, 'Installed target did not reach its final single-link state');
920
+ }
921
+ await requireBoundDirectory(parent);
922
+ }
923
+ catch (error) {
924
+ throw new AtomicCommittedMutationError(finalOutcome, error);
925
+ }
926
+ return finalOutcome;
927
+ }
928
+ export async function atomicWriteText(filePath, content, mode = 0o644, dependencyOverrides = {}) {
929
+ return atomicWriteTextInternal(filePath, content, mode, dependencyOverrides);
930
+ }
931
+ /** Write only when the target is still the exact file observed at planning time. */
932
+ export async function atomicWriteTextIfUnchanged(filePath, content, expectation, mode = 0o644, dependencyOverrides = {}) {
933
+ return atomicWriteTextInternal(filePath, content, mode, dependencyOverrides, expectation);
934
+ }
935
+ /** Delete only when the target is still the exact planned file identity and content. */
936
+ export async function unlinkRegularFileIfUnchanged(filePath, expectation, dependencyOverrides = {}) {
937
+ const parent = await bindMutationDirectory(path.dirname(filePath), expectation.expectedParentDirectory);
938
+ await assertFileExpectation(filePath, expectation);
939
+ await dependencyOverrides.beforeCommit?.(filePath);
940
+ await requireBoundDirectory(parent);
941
+ await assertFileExpectation(filePath, expectation);
942
+ const expected = expectation.expected;
943
+ if (expected === undefined)
944
+ throw changedAfterPlanning();
945
+ const plannedTarget = await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot);
946
+ if (plannedTarget === undefined || !sameSnapshot(plannedTarget.snapshot, expected)) {
947
+ throw changedAfterPlanning(filePath);
948
+ }
949
+ const quarantinePath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.deleted`);
950
+ await requireAbsentInBoundDirectory(quarantinePath, parent, 'Conditional unlink quarantine already exists', expectation.containmentRoot);
951
+ await dependencyOverrides.beforeRename?.(filePath, quarantinePath);
952
+ requireArtifact(await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot), plannedTarget, 'Planned unlink target changed during the rename hook');
953
+ await requireAbsentInBoundDirectory(quarantinePath, parent, 'Conditional unlink hook created its quarantine path', expectation.containmentRoot);
954
+ await rename(filePath, quarantinePath);
955
+ // Built-in rename fulfillment is the commit boundary. The exact original
956
+ // binding is retained before any post-rename hook or observation.
957
+ const boundQuarantined = plannedTarget;
958
+ let postRenameError;
959
+ try {
960
+ await dependencyOverrides.afterRename?.(filePath, quarantinePath);
961
+ }
962
+ catch (error) {
963
+ postRenameError = error;
964
+ }
965
+ try {
966
+ requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent, expectation.containmentRoot), boundQuarantined, 'Quarantined unlink target changed after rename');
967
+ await requireAbsentInBoundDirectory(filePath, parent, 'Conditional unlink rename left or recreated the target pathname', expectation.containmentRoot);
968
+ }
969
+ catch (error) {
970
+ throw new AtomicCommittedUnlinkError({
971
+ cleanupFailures: [await bindOwnedCleanupFailure(error, [
972
+ { artifactPath: quarantinePath, expected: boundQuarantined },
973
+ ], parent)],
974
+ }, postRenameError ?? error);
975
+ }
976
+ if (postRenameError !== undefined) {
977
+ throw new AtomicCommittedUnlinkError({
978
+ cleanupFailures: [await bindOwnedCleanupFailure(postRenameError, [
979
+ { artifactPath: quarantinePath, expected: boundQuarantined },
980
+ ], parent)],
981
+ }, postRenameError);
982
+ }
983
+ for (const absentPath of expectation.mustRemainAbsent ?? []) {
984
+ let conflict;
985
+ try {
986
+ await requireBoundDirectory(parent);
987
+ const alternate = await readRegularFile(absentPath, {
988
+ ...(expectation.containmentRoot === undefined ? {} : { containmentRoot: expectation.containmentRoot }),
989
+ });
990
+ await requireBoundDirectory(parent);
991
+ if (alternate !== undefined)
992
+ conflict = changedAfterPlanning(absentPath);
993
+ }
994
+ catch (error) {
995
+ conflict = error;
996
+ }
997
+ if (conflict !== undefined) {
998
+ const failures = await restoreQuarantinedTarget(quarantinePath, filePath, dependencyOverrides.beforeCleanup, boundQuarantined, parent);
999
+ await throwAfterOriginalRestoreAttempt(conflict, failures, filePath, expected, parent, 'Conditional unlink conflicted and rollback left an artifact');
1000
+ }
1001
+ }
1002
+ const cleanupFailure = await cleanupBoundArtifact(dependencyOverrides.beforeCleanup, quarantinePath, boundQuarantined, parent);
1003
+ const outcome = { cleanupFailures: cleanupFailure === undefined ? [] : [cleanupFailure] };
1004
+ try {
1005
+ await requireBoundDirectory(parent);
1006
+ }
1007
+ catch (error) {
1008
+ throw new AtomicCommittedUnlinkError(outcome, error);
1009
+ }
1010
+ return outcome;
35
1011
  }
36
1012
  //# sourceMappingURL=atomic-write.js.map