@starci/hfs 4.11.22 → 4.12.0

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 (1088) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +31 -31
  3. package/ci/cli.d.mts +15 -0
  4. package/ci/cli.mjs +71 -52
  5. package/ci/github.d.mts +43 -0
  6. package/ci/github.mjs +33 -28
  7. package/ci/local.d.mts +12 -0
  8. package/ci/local.mjs +145 -108
  9. package/ci/secret.d.mts +15 -0
  10. package/ci/secret.mjs +31 -21
  11. package/ci/vars.d.mts +17 -0
  12. package/ci/vars.mjs +47 -33
  13. package/ci/verify.d.mts +18 -0
  14. package/ci/verify.mjs +52 -40
  15. package/codemod/button-intent.d.mts +63 -0
  16. package/codemod/button-intent.mjs +338 -285
  17. package/coverage/cli.d.mts +28 -0
  18. package/coverage/cli.mjs +65 -51
  19. package/db/backup-receipt.d.mts +28 -0
  20. package/db/backup-receipt.mjs +78 -58
  21. package/db/drift.d.mts +86 -0
  22. package/db/drift.mjs +213 -180
  23. package/db/dump.d.mts +52 -0
  24. package/db/dump.mjs +63 -56
  25. package/db/entity-list.d.mts +4 -0
  26. package/db/entity-list.mjs +0 -2
  27. package/db/image.d.mts +8 -0
  28. package/db/image.mjs +1 -2
  29. package/db/migrate.d.mts +104 -0
  30. package/db/migrate.mjs +207 -167
  31. package/db/new.d.mts +11 -0
  32. package/db/new.mjs +25 -25
  33. package/db/post-cutover.d.mts +5 -0
  34. package/db/post-cutover.mjs +89 -82
  35. package/db/postgres.d.mts +71 -0
  36. package/db/postgres.mjs +62 -61
  37. package/emit/compiler.d.mts +3 -0
  38. package/emit/compiler.mjs +25 -27
  39. package/emit/contracts.d.mts +35 -0
  40. package/emit/contracts.mjs +46 -50
  41. package/emit/events.d.mts +9 -0
  42. package/emit/events.mjs +19 -20
  43. package/emit/form-bounds.d.mts +83 -0
  44. package/emit/form-bounds.mjs +133 -117
  45. package/emit/graphql-input-bounds.d.mts +63 -0
  46. package/emit/graphql-input-bounds.mjs +90 -79
  47. package/emit/input-bounds.d.mts +30 -0
  48. package/emit/input-bounds.mjs +29 -27
  49. package/emit/operation-schemas.d.mts +24 -0
  50. package/emit/operation-schemas.mjs +136 -116
  51. package/emit/operations-worker.d.mts +1 -0
  52. package/emit/operations-worker.mjs +9 -7
  53. package/emit/operations.d.mts +47 -0
  54. package/emit/operations.mjs +124 -133
  55. package/emit/schema-worker.d.mts +1 -0
  56. package/emit/schema-worker.mjs +57 -50
  57. package/emit/static-graph.d.mts +48 -0
  58. package/emit/static-graph.mjs +766 -640
  59. package/emit/type-schema.d.mts +29 -0
  60. package/emit/type-schema.mjs +153 -140
  61. package/lint/bound-sys.cjs +27 -33
  62. package/lint/chunk.d.mts +17 -0
  63. package/lint/chunk.mjs +41 -31
  64. package/lint/run.d.mts +120 -0
  65. package/lint/run.mjs +414 -325
  66. package/package.json +21 -4
  67. package/release/candidate-ci.d.mts +46 -0
  68. package/release/candidate-ci.mjs +76 -70
  69. package/release/gate.d.mts +120 -0
  70. package/release/gate.mjs +358 -330
  71. package/release/index.d.mts +68 -0
  72. package/release/index.mjs +95 -73
  73. package/release/lint-report.d.mts +14 -0
  74. package/release/lint-report.mjs +33 -32
  75. package/release/push.d.mts +32 -0
  76. package/release/push.mjs +92 -79
  77. package/release/records.d.mts +18 -0
  78. package/release/records.mjs +22 -21
  79. package/release/sonar-quality-gate.d.mts +18 -0
  80. package/release/sonar-quality-gate.mjs +60 -52
  81. package/release/urgent.d.mts +20 -0
  82. package/release/urgent.mjs +76 -74
  83. package/report/order.d.mts +1 -0
  84. package/report/sonar.d.mts +73 -0
  85. package/report/sonar.mjs +82 -89
  86. package/runtime/engine/admission.d.mts +209 -0
  87. package/runtime/engine/admission.mjs +225 -204
  88. package/runtime/engine/by-code-unit.d.mts +5 -0
  89. package/runtime/engine/by-code-unit.mjs +6 -6
  90. package/runtime/engine/config.d.mts +333 -0
  91. package/runtime/engine/config.mjs +486 -328
  92. package/runtime/engine/db/blob.d.mts +83 -0
  93. package/runtime/engine/db/blob.mjs +344 -268
  94. package/runtime/engine/db/event-compact.d.mts +21 -0
  95. package/runtime/engine/db/event-compact.mjs +34 -38
  96. package/runtime/engine/db/machine-connection.d.mts +68 -0
  97. package/runtime/engine/db/machine-connection.mjs +166 -119
  98. package/runtime/engine/db/machine-open.d.mts +26 -0
  99. package/runtime/engine/db/machine-open.mjs +59 -45
  100. package/runtime/engine/db/machine-schema.d.mts +34 -0
  101. package/runtime/engine/db/machine-schema.mjs +102 -83
  102. package/runtime/engine/db/machine.d.mts +1746 -0
  103. package/runtime/engine/db/machine.mjs +1040 -734
  104. package/runtime/engine/db/provider-reservations.d.mts +169 -0
  105. package/runtime/engine/db/provider-reservations.mjs +141 -119
  106. package/runtime/engine/db/provision-lock.d.mts +23 -0
  107. package/runtime/engine/db/provision-lock.mjs +14 -14
  108. package/runtime/engine/db/ref-value.d.mts +19 -0
  109. package/runtime/engine/db/ref-value.mjs +36 -35
  110. package/runtime/engine/db/sqlite-types.d.mts +28 -0
  111. package/runtime/engine/db/sqlite-types.mjs +4 -0
  112. package/runtime/engine/digest.d.mts +4 -0
  113. package/runtime/engine/digest.mjs +16 -7
  114. package/runtime/engine/invalid-config.d.mts +13 -0
  115. package/runtime/engine/invalid-config.mjs +17 -11
  116. package/runtime/engine/model-config.d.mts +91 -0
  117. package/runtime/engine/model-config.mjs +98 -76
  118. package/runtime/engine/orca-config.d.mts +26 -0
  119. package/runtime/engine/orca-config.mjs +22 -17
  120. package/runtime/engine/plain-object.d.mts +2 -0
  121. package/runtime/engine/refuse.d.mts +9 -0
  122. package/runtime/engine/refuse.mjs +3 -6
  123. package/runtime/engine/release-config.d.mts +19 -0
  124. package/runtime/engine/release-config.mjs +22 -18
  125. package/runtime/engine/removed-vocabulary.d.mts +12 -0
  126. package/runtime/engine/removed-vocabulary.mjs +21 -20
  127. package/runtime/engine/resources-config.d.mts +34 -0
  128. package/runtime/engine/resources-config.mjs +42 -35
  129. package/runtime/engine/runtime-root.d.mts +28 -0
  130. package/runtime/engine/runtime-root.mjs +11 -13
  131. package/runtime/engine/secrets.d.mts +41 -0
  132. package/runtime/engine/secrets.mjs +170 -102
  133. package/runtime/engine/sonar-config.d.mts +10 -0
  134. package/runtime/engine/sonar-config.mjs +16 -13
  135. package/runtime/engine/temp-root.d.mts +17 -0
  136. package/runtime/engine/temp-root.mjs +3 -9
  137. package/runtime/engine/yaml.d.mts +9 -0
  138. package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
  139. package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
  140. package/runtime/knowledge/hfs/rules.yaml +72 -6
  141. package/runtime/knowledge/hfs/slots.yaml +3 -3
  142. package/runtime/knowledge/sonar-gate.yaml +1 -1
  143. package/runtime/modules/kernel/failure-codes.yaml +43 -32
  144. package/runtime/modules/schemas/application-stacks.schema.yaml +3 -3
  145. package/runtime/packages/cli/src/catalog.generated.d.mts +13 -0
  146. package/runtime/packages/cli/src/catalog.generated.mjs +1853 -14824
  147. package/runtime/packages/cli/src/validate-args.d.mts +74 -0
  148. package/runtime/packages/cli/src/validate-args.mjs +183 -171
  149. package/runtime/packages/jest-preset/sonar-exclusions.cjs +5 -5
  150. package/runtime/scripts/api/fs/claim-file.d.mts +12 -0
  151. package/runtime/scripts/api/fs/claim-file.mjs +31 -17
  152. package/runtime/scripts/api/fs/ensure-temp-root.d.mts +4 -0
  153. package/runtime/scripts/api/fs/ensure-temp-root.mjs +15 -14
  154. package/runtime/scripts/api/fs/forbidden-root.d.mts +15 -0
  155. package/runtime/scripts/api/fs/forbidden-root.mjs +31 -23
  156. package/runtime/scripts/api/fs/is-link-like.d.mts +10 -0
  157. package/runtime/scripts/api/fs/is-link-like.mjs +25 -11
  158. package/runtime/scripts/api/fs/lib.d.mts +12 -0
  159. package/runtime/scripts/api/fs/lib.mjs +28 -8
  160. package/runtime/scripts/api/fs/make-temp-dir.d.mts +5 -0
  161. package/runtime/scripts/api/fs/make-temp-dir.mjs +3 -4
  162. package/runtime/scripts/api/fs/rename-over.d.mts +13 -0
  163. package/runtime/scripts/api/fs/rename-over.mjs +16 -10
  164. package/runtime/scripts/api/fs/safe-remove.d.mts +25 -0
  165. package/runtime/scripts/api/fs/safe-remove.mjs +129 -71
  166. package/runtime/scripts/api/git/add.d.mts +3 -0
  167. package/runtime/scripts/api/git/add.mjs +0 -1
  168. package/runtime/scripts/api/git/cat-file.d.mts +3 -0
  169. package/runtime/scripts/api/git/cat-file.mjs +0 -1
  170. package/runtime/scripts/api/git/check-ignore.d.mts +3 -0
  171. package/runtime/scripts/api/git/check-ignore.mjs +0 -1
  172. package/runtime/scripts/api/git/commit-tree.d.mts +3 -0
  173. package/runtime/scripts/api/git/commit-tree.mjs +0 -1
  174. package/runtime/scripts/api/git/config-get.d.mts +10 -0
  175. package/runtime/scripts/api/git/config-get.mjs +1 -2
  176. package/runtime/scripts/api/git/diff.d.mts +3 -0
  177. package/runtime/scripts/api/git/diff.mjs +0 -1
  178. package/runtime/scripts/api/git/init.d.mts +7 -0
  179. package/runtime/scripts/api/git/init.mjs +1 -2
  180. package/runtime/scripts/api/git/is-ancestor.d.mts +5 -0
  181. package/runtime/scripts/api/git/is-ancestor.mjs +0 -1
  182. package/runtime/scripts/api/git/lib.d.mts +67 -0
  183. package/runtime/scripts/api/git/lib.mjs +32 -32
  184. package/runtime/scripts/api/git/ls-files.d.mts +3 -0
  185. package/runtime/scripts/api/git/ls-files.mjs +0 -1
  186. package/runtime/scripts/api/git/ls-tree.d.mts +3 -0
  187. package/runtime/scripts/api/git/ls-tree.mjs +0 -1
  188. package/runtime/scripts/api/git/merge-base.d.mts +5 -0
  189. package/runtime/scripts/api/git/merge-base.mjs +2 -3
  190. package/runtime/scripts/api/git/remote.d.mts +3 -0
  191. package/runtime/scripts/api/git/remote.mjs +0 -1
  192. package/runtime/scripts/api/git/rev-parse-query.d.mts +3 -0
  193. package/runtime/scripts/api/git/rev-parse-query.mjs +0 -1
  194. package/runtime/scripts/api/git/show.d.mts +3 -0
  195. package/runtime/scripts/api/git/show.mjs +0 -1
  196. package/runtime/scripts/api/git/tag.d.mts +3 -0
  197. package/runtime/scripts/api/git/tag.mjs +0 -1
  198. package/runtime/scripts/api/git/update-ref.d.mts +7 -0
  199. package/runtime/scripts/api/git/update-ref.mjs +1 -2
  200. package/runtime/scripts/api/git/write-tree.d.mts +3 -0
  201. package/runtime/scripts/api/git/write-tree.mjs +0 -1
  202. package/runtime/scripts/api/node/lib.d.mts +7 -0
  203. package/runtime/scripts/api/node/lib.mjs +17 -7
  204. package/runtime/scripts/api/node/run-node.d.mts +3 -0
  205. package/runtime/scripts/api/node/run-node.mjs +0 -3
  206. package/runtime/scripts/api/process/resolve-real-tool.d.mts +12 -0
  207. package/runtime/scripts/api/process/resolve-real-tool.mjs +46 -38
  208. package/runtime/scripts/api/process/run-program.d.mts +3 -0
  209. package/runtime/scripts/api/process/run-program.mjs +5 -2
  210. package/runtime/scripts/api/sops/decrypt.d.mts +9 -0
  211. package/runtime/scripts/api/sops/decrypt.mjs +1 -2
  212. package/runtime/scripts/api/sops/lib.d.mts +149 -0
  213. package/runtime/scripts/api/sops/lib.mjs +417 -318
  214. package/runtime/scripts/api/sops/resolve-sops.d.mts +3 -0
  215. package/runtime/scripts/api/sops/resolve-sops.mjs +1 -2
  216. package/runtime/scripts/api/sops/seal.d.mts +19 -0
  217. package/runtime/scripts/api/sops/seal.mjs +14 -10
  218. package/runtime/scripts/api/sops/selected-age-recipient.d.mts +3 -0
  219. package/runtime/scripts/api/sops/selected-age-recipient.mjs +1 -2
  220. package/runtime/scripts/api/sops/update-keys.d.mts +16 -0
  221. package/runtime/scripts/api/sops/update-keys.mjs +11 -8
  222. package/runtime/scripts/cli/gen-catalog.d.mts +42 -0
  223. package/runtime/scripts/cli/gen-catalog.mjs +72 -0
  224. package/runtime/scripts/gates/runtime-host.d.mts +23 -0
  225. package/runtime/scripts/gates/runtime-host.mjs +48 -41
  226. package/runtime/scripts/gates/sonar-gate.d.mts +219 -0
  227. package/runtime/scripts/gates/sonar-gate.mjs +166 -153
  228. package/runtime/scripts/gates/starcistacks-ci.d.mts +35 -0
  229. package/runtime/scripts/gates/starcistacks-ci.mjs +47 -39
  230. package/runtime/scripts/gates/starcistacks-form.d.mts +80 -0
  231. package/runtime/scripts/gates/starcistacks-form.mjs +16 -14
  232. package/runtime/scripts/gates/starcistacks-runtime.d.mts +1 -0
  233. package/runtime/scripts/gates/starcistacks-value-file.d.mts +2 -0
  234. package/runtime/scripts/gates/starcistacks-value-file.mjs +14 -11
  235. package/runtime/scripts/gates/starcistacks.d.mts +141 -0
  236. package/runtime/scripts/gates/starcistacks.mjs +470 -355
  237. package/runtime/scripts/guards/git-pathspec.d.mts +48 -0
  238. package/runtime/scripts/guards/git-pathspec.mjs +215 -130
  239. package/runtime/scripts/guards/git-policy.d.mts +30 -0
  240. package/runtime/scripts/guards/git-policy.mjs +278 -235
  241. package/runtime/scripts/guards/hook-io.d.mts +22 -0
  242. package/runtime/scripts/guards/hook-io.mjs +77 -63
  243. package/runtime/scripts/guards/push-scan.d.mts +31 -0
  244. package/runtime/scripts/guards/push-scan.mjs +93 -80
  245. package/runtime/scripts/guards/release-push-gate.d.mts +22 -0
  246. package/runtime/scripts/guards/release-push-gate.mjs +40 -38
  247. package/runtime/scripts/hfs/allows.d.mts +24 -0
  248. package/runtime/scripts/hfs/allows.mjs +37 -38
  249. package/runtime/scripts/hfs/app-wiring.d.mts +72 -0
  250. package/runtime/scripts/hfs/app-wiring.mjs +197 -167
  251. package/runtime/scripts/hfs/architecture/ast-walks.d.mts +89 -0
  252. package/runtime/scripts/hfs/architecture/ast-walks.mjs +180 -163
  253. package/runtime/scripts/hfs/architecture/automatic-gates.d.mts +21 -0
  254. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +80 -73
  255. package/runtime/scripts/hfs/architecture/backend-app-files.d.mts +10 -0
  256. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +88 -77
  257. package/runtime/scripts/hfs/architecture/backend.d.mts +5 -0
  258. package/runtime/scripts/hfs/architecture/backend.mjs +378 -357
  259. package/runtime/scripts/hfs/architecture/background-unowned.d.mts +25 -0
  260. package/runtime/scripts/hfs/architecture/background-unowned.mjs +201 -187
  261. package/runtime/scripts/hfs/architecture/canon-clones.d.mts +8 -0
  262. package/runtime/scripts/hfs/architecture/canon-clones.mjs +260 -209
  263. package/runtime/scripts/hfs/architecture/canon-jsx-clones.d.mts +5 -0
  264. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +149 -116
  265. package/runtime/scripts/hfs/architecture/client-reaches-server.d.mts +28 -0
  266. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +72 -68
  267. package/runtime/scripts/hfs/architecture/clones.d.mts +54 -0
  268. package/runtime/scripts/hfs/architecture/clones.mjs +212 -217
  269. package/runtime/scripts/hfs/architecture/config-unread.d.mts +14 -0
  270. package/runtime/scripts/hfs/architecture/config-unread.mjs +7 -9
  271. package/runtime/scripts/hfs/architecture/config.d.mts +74 -0
  272. package/runtime/scripts/hfs/architecture/config.mjs +302 -268
  273. package/runtime/scripts/hfs/architecture/connection-map.d.mts +33 -0
  274. package/runtime/scripts/hfs/architecture/connection-map.mjs +258 -225
  275. package/runtime/scripts/hfs/architecture/constructor-deps.d.mts +57 -0
  276. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +80 -71
  277. package/runtime/scripts/hfs/architecture/context-coupling.d.mts +21 -0
  278. package/runtime/scripts/hfs/architecture/context-coupling.mjs +82 -77
  279. package/runtime/scripts/hfs/architecture/context-map.d.mts +102 -0
  280. package/runtime/scripts/hfs/architecture/context-map.mjs +267 -234
  281. package/runtime/scripts/hfs/architecture/context-owner.d.mts +15 -0
  282. package/runtime/scripts/hfs/architecture/context-owner.mjs +73 -61
  283. package/runtime/scripts/hfs/architecture/context-platform-tables.d.mts +18 -0
  284. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -43
  285. package/runtime/scripts/hfs/architecture/context-transaction.d.mts +20 -0
  286. package/runtime/scripts/hfs/architecture/context-transaction.mjs +54 -52
  287. package/runtime/scripts/hfs/architecture/contract-fixture-guard.d.mts +37 -0
  288. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +113 -104
  289. package/runtime/scripts/hfs/architecture/contracts-readonly.d.mts +32 -0
  290. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +288 -270
  291. package/runtime/scripts/hfs/architecture/contracts.d.mts +30 -0
  292. package/runtime/scripts/hfs/architecture/contracts.mjs +408 -393
  293. package/runtime/scripts/hfs/architecture/cross-app-duplicate.d.mts +27 -0
  294. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +50 -51
  295. package/runtime/scripts/hfs/architecture/dead-exports-bindings.d.mts +21 -0
  296. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +143 -119
  297. package/runtime/scripts/hfs/architecture/dead-exports.d.mts +32 -0
  298. package/runtime/scripts/hfs/architecture/dead-exports.mjs +286 -228
  299. package/runtime/scripts/hfs/architecture/default-deny.d.mts +21 -0
  300. package/runtime/scripts/hfs/architecture/default-deny.mjs +129 -110
  301. package/runtime/scripts/hfs/architecture/doc-language.d.mts +17 -0
  302. package/runtime/scripts/hfs/architecture/doc-language.mjs +24 -19
  303. package/runtime/scripts/hfs/architecture/entity-registry.d.mts +49 -0
  304. package/runtime/scripts/hfs/architecture/entity-registry.mjs +20 -22
  305. package/runtime/scripts/hfs/architecture/entrypoint.d.mts +13 -0
  306. package/runtime/scripts/hfs/architecture/entrypoint.mjs +47 -45
  307. package/runtime/scripts/hfs/architecture/error-codes.d.mts +16 -0
  308. package/runtime/scripts/hfs/architecture/error-codes.mjs +37 -35
  309. package/runtime/scripts/hfs/architecture/error-masked.d.mts +16 -0
  310. package/runtime/scripts/hfs/architecture/error-masked.mjs +55 -59
  311. package/runtime/scripts/hfs/architecture/fe-slot-allows.d.mts +38 -0
  312. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +24 -23
  313. package/runtime/scripts/hfs/architecture/feature-shape.d.mts +26 -0
  314. package/runtime/scripts/hfs/architecture/feature-shape.mjs +40 -38
  315. package/runtime/scripts/hfs/architecture/framework-pinned.d.mts +3 -0
  316. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +21 -21
  317. package/runtime/scripts/hfs/architecture/frontend-block-render.d.mts +26 -0
  318. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +106 -98
  319. package/runtime/scripts/hfs/architecture/frontend-grammar.d.mts +7 -0
  320. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +153 -150
  321. package/runtime/scripts/hfs/architecture/frontend-routing.d.mts +23 -0
  322. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +161 -149
  323. package/runtime/scripts/hfs/architecture/frontend-world-render.d.mts +72 -0
  324. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +343 -305
  325. package/runtime/scripts/hfs/architecture/frontend-world.d.mts +94 -0
  326. package/runtime/scripts/hfs/architecture/frontend-world.mjs +340 -294
  327. package/runtime/scripts/hfs/architecture/frontend.d.mts +31 -0
  328. package/runtime/scripts/hfs/architecture/frontend.mjs +174 -184
  329. package/runtime/scripts/hfs/architecture/function-spec-providers.d.mts +27 -0
  330. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +124 -106
  331. package/runtime/scripts/hfs/architecture/hfs-graph.d.mts +78 -0
  332. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +52 -48
  333. package/runtime/scripts/hfs/architecture/hfs.d.mts +135 -0
  334. package/runtime/scripts/hfs/architecture/hfs.mjs +328 -282
  335. package/runtime/scripts/hfs/architecture/hooks-are-hooks.d.mts +24 -0
  336. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +81 -71
  337. package/runtime/scripts/hfs/architecture/i18n-keys.d.mts +31 -0
  338. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +155 -141
  339. package/runtime/scripts/hfs/architecture/identity.d.mts +7 -0
  340. package/runtime/scripts/hfs/architecture/identity.mjs +141 -125
  341. package/runtime/scripts/hfs/architecture/index.d.mts +101 -0
  342. package/runtime/scripts/hfs/architecture/index.mjs +272 -272
  343. package/runtime/scripts/hfs/architecture/injection-token-exported.d.mts +21 -0
  344. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +45 -43
  345. package/runtime/scripts/hfs/architecture/jwt-ast.d.mts +16 -0
  346. package/runtime/scripts/hfs/architecture/jwt-ast.mjs +195 -176
  347. package/runtime/scripts/hfs/architecture/machine-ast.d.mts +88 -0
  348. package/runtime/scripts/hfs/architecture/machine-ast.mjs +163 -155
  349. package/runtime/scripts/hfs/architecture/managed-scripts.d.mts +2 -0
  350. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +19 -20
  351. package/runtime/scripts/hfs/architecture/module-per-transport.d.mts +35 -0
  352. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +177 -172
  353. package/runtime/scripts/hfs/architecture/next-data-contract.d.mts +35 -0
  354. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +100 -78
  355. package/runtime/scripts/hfs/architecture/next-data-inference.d.mts +39 -0
  356. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +170 -154
  357. package/runtime/scripts/hfs/architecture/next-data-key.d.mts +50 -0
  358. package/runtime/scripts/hfs/architecture/next-data-key.mjs +311 -275
  359. package/runtime/scripts/hfs/architecture/next-data-symbols.d.mts +5 -0
  360. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +17 -17
  361. package/runtime/scripts/hfs/architecture/next-data.d.mts +36 -0
  362. package/runtime/scripts/hfs/architecture/next-data.mjs +351 -334
  363. package/runtime/scripts/hfs/architecture/operation-contract-roots.d.mts +10 -0
  364. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +39 -37
  365. package/runtime/scripts/hfs/architecture/owners.d.mts +5 -0
  366. package/runtime/scripts/hfs/architecture/owners.mjs +97 -98
  367. package/runtime/scripts/hfs/architecture/package-shape.d.mts +26 -0
  368. package/runtime/scripts/hfs/architecture/package-shape.mjs +56 -55
  369. package/runtime/scripts/hfs/architecture/presentation.d.mts +24 -0
  370. package/runtime/scripts/hfs/architecture/presentation.mjs +138 -130
  371. package/runtime/scripts/hfs/architecture/reachability.d.mts +25 -0
  372. package/runtime/scripts/hfs/architecture/reachability.mjs +258 -241
  373. package/runtime/scripts/hfs/architecture/register-once.d.mts +18 -0
  374. package/runtime/scripts/hfs/architecture/register-once.mjs +152 -139
  375. package/runtime/scripts/hfs/architecture/registration.d.mts +15 -0
  376. package/runtime/scripts/hfs/architecture/registration.mjs +301 -267
  377. package/runtime/scripts/hfs/architecture/required-files.d.mts +64 -0
  378. package/runtime/scripts/hfs/architecture/required-files.mjs +172 -167
  379. package/runtime/scripts/hfs/architecture/route-files-thin.d.mts +23 -0
  380. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +85 -77
  381. package/runtime/scripts/hfs/architecture/schema-owner.d.mts +34 -0
  382. package/runtime/scripts/hfs/architecture/schema-owner.mjs +206 -181
  383. package/runtime/scripts/hfs/architecture/server-action.d.mts +22 -0
  384. package/runtime/scripts/hfs/architecture/server-action.mjs +48 -48
  385. package/runtime/scripts/hfs/architecture/side-boundary.d.mts +3 -0
  386. package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
  387. package/runtime/scripts/hfs/architecture/source-names-shape.d.mts +85 -0
  388. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +261 -259
  389. package/runtime/scripts/hfs/architecture/source-names.d.mts +7 -0
  390. package/runtime/scripts/hfs/architecture/source-names.mjs +304 -278
  391. package/runtime/scripts/hfs/architecture/sql-keys.d.mts +5 -0
  392. package/runtime/scripts/hfs/architecture/sql-keys.mjs +277 -194
  393. package/runtime/scripts/hfs/architecture/sql-owner.d.mts +30 -0
  394. package/runtime/scripts/hfs/architecture/sql-owner.mjs +236 -194
  395. package/runtime/scripts/hfs/architecture/sql-returning.d.mts +22 -0
  396. package/runtime/scripts/hfs/architecture/sql-returning.mjs +46 -45
  397. package/runtime/scripts/hfs/architecture/sql-token-forms.d.mts +21 -0
  398. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +86 -74
  399. package/runtime/scripts/hfs/architecture/sql-tokens.d.mts +32 -0
  400. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +456 -341
  401. package/runtime/scripts/hfs/architecture/staged-consumers.d.mts +7 -0
  402. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +35 -30
  403. package/runtime/scripts/hfs/architecture/surface.d.mts +26 -0
  404. package/runtime/scripts/hfs/architecture/surface.mjs +64 -74
  405. package/runtime/scripts/hfs/architecture/symbols.d.mts +11 -0
  406. package/runtime/scripts/hfs/architecture/symbols.mjs +209 -216
  407. package/runtime/scripts/hfs/architecture/test-world-files.d.mts +45 -0
  408. package/runtime/scripts/hfs/architecture/test-world-files.mjs +165 -155
  409. package/runtime/scripts/hfs/architecture/tiers.d.mts +29 -0
  410. package/runtime/scripts/hfs/architecture/tiers.mjs +161 -154
  411. package/runtime/scripts/hfs/architecture/transport-owner.d.mts +37 -0
  412. package/runtime/scripts/hfs/architecture/transport-owner.mjs +155 -149
  413. package/runtime/scripts/hfs/architecture/type-context.d.mts +60 -0
  414. package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
  415. package/runtime/scripts/hfs/architecture/typescript.d.mts +94 -0
  416. package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
  417. package/runtime/scripts/hfs/architecture/unit-spec-providers.d.mts +34 -0
  418. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +110 -92
  419. package/runtime/scripts/hfs/architecture.d.mts +13 -0
  420. package/runtime/scripts/hfs/architecture.mjs +39 -29
  421. package/runtime/scripts/hfs/canon-pins.d.mts +26 -0
  422. package/runtime/scripts/hfs/canon-pins.mjs +5 -8
  423. package/runtime/scripts/hfs/catalog-once.d.mts +2 -0
  424. package/runtime/scripts/hfs/catalog-once.mjs +8 -9
  425. package/runtime/scripts/hfs/check-results.d.mts +29 -0
  426. package/runtime/scripts/hfs/check-results.mjs +14 -18
  427. package/runtime/scripts/hfs/check.d.mts +144 -0
  428. package/runtime/scripts/hfs/check.mjs +309 -329
  429. package/runtime/scripts/hfs/ci-declaration.d.mts +77 -0
  430. package/runtime/scripts/hfs/ci-declaration.mjs +99 -87
  431. package/runtime/scripts/hfs/coverage-from.d.mts +22 -0
  432. package/runtime/scripts/hfs/coverage-from.mjs +52 -49
  433. package/runtime/scripts/hfs/coverage-scope.d.mts +87 -0
  434. package/runtime/scripts/hfs/coverage-scope.mjs +139 -142
  435. package/runtime/scripts/hfs/db-baseline-verb.d.mts +84 -0
  436. package/runtime/scripts/hfs/db-baseline-verb.mjs +327 -266
  437. package/runtime/scripts/hfs/db-errors.d.mts +2 -0
  438. package/runtime/scripts/hfs/db-errors.mjs +2 -1
  439. package/runtime/scripts/hfs/db-plan-verb.d.mts +33 -0
  440. package/runtime/scripts/hfs/db-plan-verb.mjs +174 -128
  441. package/runtime/scripts/hfs/db-restamp-verb.d.mts +5 -0
  442. package/runtime/scripts/hfs/db-restamp-verb.mjs +94 -70
  443. package/runtime/scripts/hfs/db-squash.d.mts +117 -0
  444. package/runtime/scripts/hfs/db-squash.mjs +414 -351
  445. package/runtime/scripts/hfs/declaration-shape.d.mts +3 -0
  446. package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
  447. package/runtime/scripts/hfs/declaration-slots.d.mts +18 -0
  448. package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
  449. package/runtime/scripts/hfs/doc-commands.d.mts +71 -0
  450. package/runtime/scripts/hfs/doc-commands.mjs +257 -250
  451. package/runtime/scripts/hfs/edition-slots.d.mts +37 -0
  452. package/runtime/scripts/hfs/edition-slots.mjs +43 -40
  453. package/runtime/scripts/hfs/example-secrets.d.mts +18 -0
  454. package/runtime/scripts/hfs/example-secrets.mjs +173 -154
  455. package/runtime/scripts/hfs/gate-threshold.d.mts +20 -0
  456. package/runtime/scripts/hfs/gate-threshold.mjs +14 -22
  457. package/runtime/scripts/hfs/grammar-presentation.d.mts +74 -0
  458. package/runtime/scripts/hfs/grammar-presentation.mjs +109 -106
  459. package/runtime/scripts/hfs/linear-text.d.mts +6 -0
  460. package/runtime/scripts/hfs/linear-text.mjs +20 -20
  461. package/runtime/scripts/hfs/manifest-shape.d.mts +216 -0
  462. package/runtime/scripts/hfs/manifest-shape.mjs +399 -324
  463. package/runtime/scripts/hfs/param-names.d.mts +3 -0
  464. package/runtime/scripts/hfs/param-names.mjs +6 -11
  465. package/runtime/scripts/hfs/path-findings.d.mts +22 -0
  466. package/runtime/scripts/hfs/path-findings.mjs +120 -100
  467. package/runtime/scripts/hfs/pin-findings.d.mts +19 -0
  468. package/runtime/scripts/hfs/pin-findings.mjs +26 -21
  469. package/runtime/scripts/hfs/refusal-codes.d.mts +1 -0
  470. package/runtime/scripts/hfs/refusal-codes.mjs +2 -2
  471. package/runtime/scripts/hfs/repo-identity.d.mts +15 -0
  472. package/runtime/scripts/hfs/repo-identity.mjs +55 -42
  473. package/runtime/scripts/hfs/rule-catalog.d.mts +77 -0
  474. package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
  475. package/runtime/scripts/hfs/rule-params-shape.d.mts +10 -0
  476. package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
  477. package/runtime/scripts/hfs/rules/a15-findings.d.mts +18 -0
  478. package/runtime/scripts/hfs/rules/a15-findings.mjs +59 -55
  479. package/runtime/scripts/hfs/rules/app-env-required.d.mts +9 -0
  480. package/runtime/scripts/hfs/rules/app-env-required.mjs +13 -13
  481. package/runtime/scripts/hfs/rules/app-root.d.mts +15 -0
  482. package/runtime/scripts/hfs/rules/app-root.mjs +20 -21
  483. package/runtime/scripts/hfs/rules/app-workflow-findings.d.mts +12 -0
  484. package/runtime/scripts/hfs/rules/app-workflow-findings.mjs +4 -4
  485. package/runtime/scripts/hfs/rules/assets.d.mts +13 -0
  486. package/runtime/scripts/hfs/rules/assets.mjs +58 -46
  487. package/runtime/scripts/hfs/rules/boot-proof.d.mts +11 -0
  488. package/runtime/scripts/hfs/rules/boot-proof.mjs +7 -8
  489. package/runtime/scripts/hfs/rules/bypass-markers.d.mts +9 -0
  490. package/runtime/scripts/hfs/rules/bypass-markers.mjs +7 -12
  491. package/runtime/scripts/hfs/rules/canon-copy-drift.d.mts +10 -0
  492. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +70 -68
  493. package/runtime/scripts/hfs/rules/ci-custody.d.mts +7 -0
  494. package/runtime/scripts/hfs/rules/ci-custody.mjs +72 -73
  495. package/runtime/scripts/hfs/rules/ci-standard.d.mts +12 -0
  496. package/runtime/scripts/hfs/rules/ci-standard.mjs +149 -142
  497. package/runtime/scripts/hfs/rules/cli.d.mts +7 -0
  498. package/runtime/scripts/hfs/rules/cli.mjs +24 -23
  499. package/runtime/scripts/hfs/rules/contract-compat.d.mts +11 -0
  500. package/runtime/scripts/hfs/rules/contract-compat.mjs +64 -53
  501. package/runtime/scripts/hfs/rules/contract.d.mts +26 -0
  502. package/runtime/scripts/hfs/rules/contract.mjs +126 -111
  503. package/runtime/scripts/hfs/rules/coverage-below.d.mts +17 -0
  504. package/runtime/scripts/hfs/rules/coverage-below.mjs +200 -167
  505. package/runtime/scripts/hfs/rules/database-access.d.mts +12 -0
  506. package/runtime/scripts/hfs/rules/database-access.mjs +231 -202
  507. package/runtime/scripts/hfs/rules/database-baseline.d.mts +22 -0
  508. package/runtime/scripts/hfs/rules/database-baseline.mjs +104 -104
  509. package/runtime/scripts/hfs/rules/database-config.d.mts +11 -0
  510. package/runtime/scripts/hfs/rules/database-config.mjs +101 -135
  511. package/runtime/scripts/hfs/rules/database-constants.d.mts +36 -0
  512. package/runtime/scripts/hfs/rules/database-constants.mjs +7 -7
  513. package/runtime/scripts/hfs/rules/database-migrations.d.mts +41 -0
  514. package/runtime/scripts/hfs/rules/database-migrations.mjs +428 -388
  515. package/runtime/scripts/hfs/rules/database-plpgsql.d.mts +5 -0
  516. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +278 -259
  517. package/runtime/scripts/hfs/rules/database-queue-sources.d.mts +4 -0
  518. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +48 -43
  519. package/runtime/scripts/hfs/rules/database-queues.d.mts +15 -0
  520. package/runtime/scripts/hfs/rules/database-queues.mjs +211 -182
  521. package/runtime/scripts/hfs/rules/database-sql.d.mts +16 -0
  522. package/runtime/scripts/hfs/rules/database-sql.mjs +394 -394
  523. package/runtime/scripts/hfs/rules/database-transaction.d.mts +3 -0
  524. package/runtime/scripts/hfs/rules/database-transaction.mjs +25 -27
  525. package/runtime/scripts/hfs/rules/database.d.mts +22 -0
  526. package/runtime/scripts/hfs/rules/database.mjs +42 -58
  527. package/runtime/scripts/hfs/rules/deploy-scan.d.mts +10 -0
  528. package/runtime/scripts/hfs/rules/deploy-scan.mjs +177 -168
  529. package/runtime/scripts/hfs/rules/deploy-workflow.d.mts +3 -0
  530. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +194 -171
  531. package/runtime/scripts/hfs/rules/deps.d.mts +6 -0
  532. package/runtime/scripts/hfs/rules/deps.mjs +61 -60
  533. package/runtime/scripts/hfs/rules/doc-fresh.d.mts +11 -0
  534. package/runtime/scripts/hfs/rules/doc-fresh.mjs +242 -223
  535. package/runtime/scripts/hfs/rules/docker-health.d.mts +9 -0
  536. package/runtime/scripts/hfs/rules/docker-health.mjs +222 -196
  537. package/runtime/scripts/hfs/rules/docker.d.mts +13 -0
  538. package/runtime/scripts/hfs/rules/docker.mjs +234 -208
  539. package/runtime/scripts/hfs/rules/edition.d.mts +18 -0
  540. package/runtime/scripts/hfs/rules/edition.mjs +103 -101
  541. package/runtime/scripts/hfs/rules/env-parity.d.mts +25 -0
  542. package/runtime/scripts/hfs/rules/env-parity.mjs +278 -252
  543. package/runtime/scripts/hfs/rules/env-value-shape.d.mts +21 -0
  544. package/runtime/scripts/hfs/rules/env-value-shape.mjs +82 -67
  545. package/runtime/scripts/hfs/rules/event-bus.d.mts +17 -0
  546. package/runtime/scripts/hfs/rules/event-bus.mjs +145 -133
  547. package/runtime/scripts/hfs/rules/event-tables.d.mts +7 -0
  548. package/runtime/scripts/hfs/rules/event-tables.mjs +77 -75
  549. package/runtime/scripts/hfs/rules/fe-build-args.d.mts +10 -0
  550. package/runtime/scripts/hfs/rules/fe-build-args.mjs +116 -99
  551. package/runtime/scripts/hfs/rules/fe-contract-documents.d.mts +7 -0
  552. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +121 -111
  553. package/runtime/scripts/hfs/rules/fe-no-tests.d.mts +9 -0
  554. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +37 -36
  555. package/runtime/scripts/hfs/rules/fe-stack.d.mts +23 -0
  556. package/runtime/scripts/hfs/rules/fe-stack.mjs +108 -105
  557. package/runtime/scripts/hfs/rules/feature-files.d.mts +10 -0
  558. package/runtime/scripts/hfs/rules/feature-files.mjs +14 -19
  559. package/runtime/scripts/hfs/rules/frontend-tree.d.mts +18 -0
  560. package/runtime/scripts/hfs/rules/frontend-tree.mjs +105 -91
  561. package/runtime/scripts/hfs/rules/integration-specs.d.mts +7 -0
  562. package/runtime/scripts/hfs/rules/integration-specs.mjs +190 -177
  563. package/runtime/scripts/hfs/rules/kinds.d.mts +6 -0
  564. package/runtime/scripts/hfs/rules/kinds.mjs +88 -81
  565. package/runtime/scripts/hfs/rules/lexicon.d.mts +9 -0
  566. package/runtime/scripts/hfs/rules/lexicon.mjs +43 -40
  567. package/runtime/scripts/hfs/rules/lint-suppression.d.mts +5 -0
  568. package/runtime/scripts/hfs/rules/lint-suppression.mjs +22 -20
  569. package/runtime/scripts/hfs/rules/lite-proof-lane.d.mts +9 -0
  570. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +59 -58
  571. package/runtime/scripts/hfs/rules/lite-secrets.d.mts +12 -0
  572. package/runtime/scripts/hfs/rules/lite-secrets.mjs +60 -63
  573. package/runtime/scripts/hfs/rules/mcp-tools.d.mts +7 -0
  574. package/runtime/scripts/hfs/rules/mcp-tools.mjs +56 -41
  575. package/runtime/scripts/hfs/rules/monorepo.d.mts +17 -0
  576. package/runtime/scripts/hfs/rules/monorepo.mjs +153 -138
  577. package/runtime/scripts/hfs/rules/peer-integrations.d.mts +7 -0
  578. package/runtime/scripts/hfs/rules/peer-integrations.mjs +39 -36
  579. package/runtime/scripts/hfs/rules/pipeline.d.mts +7 -0
  580. package/runtime/scripts/hfs/rules/pipeline.mjs +53 -53
  581. package/runtime/scripts/hfs/rules/proof-commands.d.mts +14 -0
  582. package/runtime/scripts/hfs/rules/proof-commands.mjs +52 -41
  583. package/runtime/scripts/hfs/rules/read.d.mts +7 -0
  584. package/runtime/scripts/hfs/rules/read.mjs +19 -15
  585. package/runtime/scripts/hfs/rules/release-provenance.d.mts +12 -0
  586. package/runtime/scripts/hfs/rules/release-provenance.mjs +196 -159
  587. package/runtime/scripts/hfs/rules/repo-local-checks.d.mts +5 -0
  588. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +19 -17
  589. package/runtime/scripts/hfs/rules/rule-types.d.mts +40 -0
  590. package/runtime/scripts/hfs/rules/rule-types.mjs +4 -0
  591. package/runtime/scripts/hfs/rules/saga-delivery.d.mts +2 -0
  592. package/runtime/scripts/hfs/rules/saga-delivery.mjs +58 -50
  593. package/runtime/scripts/hfs/rules/saga.d.mts +7 -0
  594. package/runtime/scripts/hfs/rules/saga.mjs +151 -145
  595. package/runtime/scripts/hfs/rules/script-hygiene.d.mts +11 -0
  596. package/runtime/scripts/hfs/rules/script-hygiene.mjs +112 -95
  597. package/runtime/scripts/hfs/rules/secret-custody-only.d.mts +7 -0
  598. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +29 -35
  599. package/runtime/scripts/hfs/rules/secrets.d.mts +29 -0
  600. package/runtime/scripts/hfs/rules/secrets.mjs +45 -44
  601. package/runtime/scripts/hfs/rules/services.d.mts +7 -0
  602. package/runtime/scripts/hfs/rules/services.mjs +148 -148
  603. package/runtime/scripts/hfs/rules/spec-placement.d.mts +8 -0
  604. package/runtime/scripts/hfs/rules/spec-placement.mjs +12 -13
  605. package/runtime/scripts/hfs/rules/stack-tooling.d.mts +11 -0
  606. package/runtime/scripts/hfs/rules/stack-tooling.mjs +45 -52
  607. package/runtime/scripts/hfs/rules/stacks-auth-cookie-domain.d.mts +30 -0
  608. package/runtime/scripts/hfs/rules/stacks-auth-cookie-domain.mjs +149 -0
  609. package/runtime/scripts/hfs/rules/stacks-deploy.d.mts +7 -0
  610. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +32 -42
  611. package/runtime/scripts/hfs/rules/stacks-ingress.d.mts +16 -0
  612. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +62 -64
  613. package/runtime/scripts/hfs/rules/stacks-lite-backing.d.mts +17 -0
  614. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -116
  615. package/runtime/scripts/hfs/rules/stacks-release.d.mts +26 -0
  616. package/runtime/scripts/hfs/rules/stacks-release.mjs +335 -309
  617. package/runtime/scripts/hfs/rules/stacks-runtime.d.mts +31 -0
  618. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +130 -124
  619. package/runtime/scripts/hfs/rules/stacks-swarm.d.mts +5 -0
  620. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +366 -344
  621. package/runtime/scripts/hfs/rules/stacks.d.mts +7 -0
  622. package/runtime/scripts/hfs/rules/stacks.mjs +37 -38
  623. package/runtime/scripts/hfs/rules/staged-specs.d.mts +10 -0
  624. package/runtime/scripts/hfs/rules/staged-specs.mjs +14 -18
  625. package/runtime/scripts/hfs/rules/supply-chain.d.mts +10 -0
  626. package/runtime/scripts/hfs/rules/supply-chain.mjs +143 -150
  627. package/runtime/scripts/hfs/rules/test-topology.d.mts +5 -0
  628. package/runtime/scripts/hfs/rules/test-topology.mjs +23 -21
  629. package/runtime/scripts/hfs/rules/text-integrity.d.mts +9 -0
  630. package/runtime/scripts/hfs/rules/text-integrity.mjs +47 -43
  631. package/runtime/scripts/hfs/rules/unused-symbol-flow.d.mts +22 -0
  632. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +226 -204
  633. package/runtime/scripts/hfs/rules/unused-symbol-ledger.d.mts +7 -0
  634. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +70 -49
  635. package/runtime/scripts/hfs/rules/unused-symbol-properties.d.mts +12 -0
  636. package/runtime/scripts/hfs/rules/unused-symbol-properties.mjs +141 -126
  637. package/runtime/scripts/hfs/rules/unused-symbol-translators.d.mts +8 -0
  638. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +160 -140
  639. package/runtime/scripts/hfs/rules/unused-symbol-values.d.mts +9 -0
  640. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +138 -125
  641. package/runtime/scripts/hfs/rules/unused-symbol.d.mts +11 -0
  642. package/runtime/scripts/hfs/rules/unused-symbol.mjs +283 -253
  643. package/runtime/scripts/hfs/rules/workflow-contexts.d.mts +8 -0
  644. package/runtime/scripts/hfs/rules/workflow-contexts.mjs +79 -67
  645. package/runtime/scripts/hfs/runtime-rules/git-triggers.d.mts +6 -0
  646. package/runtime/scripts/hfs/runtime-rules/git-triggers.mjs +36 -35
  647. package/runtime/scripts/hfs/runtime-rules/hook-shape.d.mts +10 -0
  648. package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +45 -42
  649. package/runtime/scripts/hfs/runtime-rules/source-ast.d.mts +39 -0
  650. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +130 -116
  651. package/runtime/scripts/hfs/runtime-rules/workflow-source.d.mts +5 -0
  652. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +8 -7
  653. package/runtime/scripts/hfs/sealed-members.d.mts +22 -0
  654. package/runtime/scripts/hfs/sealed-members.mjs +79 -75
  655. package/runtime/scripts/hfs/secret-bootstrap-crypto.d.mts +12 -0
  656. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +151 -167
  657. package/runtime/scripts/hfs/secret-document.d.mts +16 -0
  658. package/runtime/scripts/hfs/secret-document.mjs +59 -75
  659. package/runtime/scripts/hfs/secret-edit.d.mts +14 -0
  660. package/runtime/scripts/hfs/secret-edit.mjs +115 -135
  661. package/runtime/scripts/hfs/secret-errors.d.mts +4 -0
  662. package/runtime/scripts/hfs/secret-errors.mjs +19 -16
  663. package/runtime/scripts/hfs/secret-files.d.mts +20 -0
  664. package/runtime/scripts/hfs/secret-files.mjs +103 -112
  665. package/runtime/scripts/hfs/secret-init.d.mts +108 -0
  666. package/runtime/scripts/hfs/secret-init.mjs +304 -346
  667. package/runtime/scripts/hfs/secret-output.d.mts +18 -0
  668. package/runtime/scripts/hfs/secret-output.mjs +52 -80
  669. package/runtime/scripts/hfs/secret-prompt.d.mts +39 -0
  670. package/runtime/scripts/hfs/secret-prompt.mjs +108 -93
  671. package/runtime/scripts/hfs/secret-sops.d.mts +29 -0
  672. package/runtime/scripts/hfs/secret-sops.mjs +161 -199
  673. package/runtime/scripts/hfs/secret-standard.d.mts +89 -0
  674. package/runtime/scripts/hfs/secret-standard.mjs +300 -264
  675. package/runtime/scripts/hfs/secret-store.d.mts +36 -0
  676. package/runtime/scripts/hfs/secret-store.mjs +120 -136
  677. package/runtime/scripts/hfs/secret.d.mts +40 -0
  678. package/runtime/scripts/hfs/secret.mjs +254 -222
  679. package/runtime/scripts/hfs/slot-app-view.d.mts +11 -0
  680. package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
  681. package/runtime/scripts/hfs/slot-classify.d.mts +75 -0
  682. package/runtime/scripts/hfs/slot-classify.mjs +42 -38
  683. package/runtime/scripts/hfs/slot-errors.d.mts +6 -0
  684. package/runtime/scripts/hfs/slot-errors.mjs +8 -7
  685. package/runtime/scripts/hfs/slot-imports.d.mts +8 -0
  686. package/runtime/scripts/hfs/slot-imports.mjs +70 -63
  687. package/runtime/scripts/hfs/slot-manifest-shape.d.mts +9 -0
  688. package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
  689. package/runtime/scripts/hfs/slot-match.d.mts +50 -0
  690. package/runtime/scripts/hfs/slot-match.mjs +122 -111
  691. package/runtime/scripts/hfs/slot-path.d.mts +1 -0
  692. package/runtime/scripts/hfs/slot-path.mjs +0 -2
  693. package/runtime/scripts/hfs/slot-required.d.mts +11 -0
  694. package/runtime/scripts/hfs/slot-required.mjs +65 -59
  695. package/runtime/scripts/hfs/slot-semantic-problems.d.mts +9 -0
  696. package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
  697. package/runtime/scripts/hfs/slot-side-problems.d.mts +3 -0
  698. package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
  699. package/runtime/scripts/hfs/slots.d.mts +188 -0
  700. package/runtime/scripts/hfs/slots.mjs +175 -152
  701. package/runtime/scripts/hfs/sql/pg-parse.d.mts +9 -0
  702. package/runtime/scripts/hfs/sql/pg-parse.mjs +25 -25
  703. package/runtime/scripts/hfs/stack-config-kit.d.mts +22 -0
  704. package/runtime/scripts/hfs/stack-config-kit.mjs +21 -17
  705. package/runtime/scripts/hfs/stack-deploy-config.d.mts +155 -0
  706. package/runtime/scripts/hfs/stack-deploy-config.mjs +345 -273
  707. package/runtime/scripts/hfs/stack-ingress-config.d.mts +74 -0
  708. package/runtime/scripts/hfs/stack-ingress-config.mjs +211 -177
  709. package/runtime/scripts/hfs/stacks-gate.d.mts +52 -0
  710. package/runtime/scripts/hfs/stacks-gate.mjs +557 -397
  711. package/runtime/scripts/hfs/starcistacks-runtime.d.mts +7 -0
  712. package/runtime/scripts/hfs/starcistacks-runtime.mjs +51 -40
  713. package/runtime/scripts/hfs/test-world-slot.d.mts +2 -0
  714. package/runtime/scripts/hfs/test-world-slot.mjs +0 -1
  715. package/runtime/scripts/hfs/trailing-slashes.d.mts +2 -0
  716. package/runtime/scripts/hfs/trailing-slashes.mjs +4 -4
  717. package/runtime/scripts/hfs/tree-findings.d.mts +9 -0
  718. package/runtime/scripts/hfs/tree-findings.mjs +18 -17
  719. package/runtime/scripts/hfs/tree.d.mts +35 -0
  720. package/runtime/scripts/hfs/tree.mjs +56 -46
  721. package/runtime/scripts/hfs/typescript-programs.d.mts +36 -0
  722. package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
  723. package/runtime/scripts/hfs/view.d.mts +45 -0
  724. package/runtime/scripts/hfs/view.mjs +38 -40
  725. package/runtime/scripts/lib/bypass-markers.d.mts +15 -0
  726. package/runtime/scripts/lib/bypass-markers.mjs +210 -197
  727. package/runtime/scripts/lib/cli-arg.d.mts +67 -0
  728. package/runtime/scripts/lib/cli-arg.mjs +59 -63
  729. package/runtime/scripts/lib/command-tokens.d.mts +11 -0
  730. package/runtime/scripts/lib/command-tokens.mjs +77 -78
  731. package/runtime/scripts/lib/compose-values.d.mts +6 -0
  732. package/runtime/scripts/lib/compose-values.mjs +7 -7
  733. package/runtime/scripts/lib/css-rules.d.mts +36 -0
  734. package/runtime/scripts/lib/css-rules.mjs +181 -171
  735. package/runtime/scripts/lib/deployment-findings.d.mts +9 -0
  736. package/runtime/scripts/lib/deployment-findings.mjs +4 -4
  737. package/runtime/scripts/lib/dockerfile.d.mts +39 -0
  738. package/runtime/scripts/lib/dockerfile.mjs +131 -114
  739. package/runtime/scripts/lib/dom-tree.d.mts +7 -0
  740. package/runtime/scripts/lib/dom-tree.mjs +3 -3
  741. package/runtime/scripts/lib/env.d.mts +12 -0
  742. package/runtime/scripts/lib/env.mjs +3 -6
  743. package/runtime/scripts/lib/escape.d.mts +6 -0
  744. package/runtime/scripts/lib/escape.mjs +2 -6
  745. package/runtime/scripts/lib/event-contract.d.mts +21 -0
  746. package/runtime/scripts/lib/event-contract.mjs +73 -78
  747. package/runtime/scripts/lib/event-items.d.mts +20 -0
  748. package/runtime/scripts/lib/event-items.mjs +27 -15
  749. package/runtime/scripts/lib/example-refs.d.mts +69 -0
  750. package/runtime/scripts/lib/example-refs.mjs +145 -129
  751. package/runtime/scripts/lib/fs-kind.d.mts +12 -0
  752. package/runtime/scripts/lib/fs-kind.mjs +20 -10
  753. package/runtime/scripts/lib/git-hooks-dir.d.mts +15 -0
  754. package/runtime/scripts/lib/git-hooks-dir.mjs +16 -16
  755. package/runtime/scripts/lib/git.d.mts +24 -0
  756. package/runtime/scripts/lib/git.mjs +15 -16
  757. package/runtime/scripts/lib/glob.d.mts +4 -0
  758. package/runtime/scripts/lib/glob.mjs +23 -14
  759. package/runtime/scripts/lib/grammar-lint.d.mts +47 -0
  760. package/runtime/scripts/lib/grammar-lint.mjs +33 -37
  761. package/runtime/scripts/lib/grammar-vendor-parity-jsx.d.mts +40 -0
  762. package/runtime/scripts/lib/grammar-vendor-parity-jsx.mjs +285 -266
  763. package/runtime/scripts/lib/grammar-vendor-parity.d.mts +61 -0
  764. package/runtime/scripts/lib/grammar-vendor-parity.mjs +209 -216
  765. package/runtime/scripts/lib/graphql-contract.d.mts +53 -0
  766. package/runtime/scripts/lib/graphql-contract.mjs +201 -198
  767. package/runtime/scripts/lib/graphql-sdl.d.mts +66 -0
  768. package/runtime/scripts/lib/graphql-sdl.mjs +230 -205
  769. package/runtime/scripts/lib/hook-limits.d.mts +11 -0
  770. package/runtime/scripts/lib/hook-limits.mjs +0 -1
  771. package/runtime/scripts/lib/i18n.d.mts +48 -0
  772. package/runtime/scripts/lib/i18n.mjs +32 -35
  773. package/runtime/scripts/lib/in-order.d.mts +12 -0
  774. package/runtime/scripts/lib/in-order.mjs +57 -38
  775. package/runtime/scripts/lib/is-main.d.mts +6 -0
  776. package/runtime/scripts/lib/is-main.mjs +11 -4
  777. package/runtime/scripts/lib/js-lex.d.mts +32 -0
  778. package/runtime/scripts/lib/js-lex.mjs +196 -162
  779. package/runtime/scripts/lib/json-clone.d.mts +11 -0
  780. package/runtime/scripts/lib/json-clone.mjs +45 -36
  781. package/runtime/scripts/lib/json-schema.d.mts +2 -0
  782. package/runtime/scripts/lib/json-schema.mjs +127 -90
  783. package/runtime/scripts/lib/json.d.mts +16 -0
  784. package/runtime/scripts/lib/json.mjs +26 -14
  785. package/runtime/scripts/lib/language.d.mts +44 -0
  786. package/runtime/scripts/lib/language.mjs +133 -129
  787. package/runtime/scripts/lib/list.d.mts +17 -0
  788. package/runtime/scripts/lib/list.mjs +9 -14
  789. package/runtime/scripts/lib/loosening-budgets.d.mts +12 -0
  790. package/runtime/scripts/lib/loosening-budgets.mjs +35 -29
  791. package/runtime/scripts/lib/loosening-config.d.mts +64 -0
  792. package/runtime/scripts/lib/loosening-config.mjs +267 -236
  793. package/runtime/scripts/lib/loosening-state.d.mts +77 -0
  794. package/runtime/scripts/lib/loosening-state.mjs +134 -128
  795. package/runtime/scripts/lib/mutation-fence.d.mts +11 -0
  796. package/runtime/scripts/lib/mutation-fence.mjs +10 -5
  797. package/runtime/scripts/lib/normalize.d.mts +12 -0
  798. package/runtime/scripts/lib/normalize.mjs +11 -11
  799. package/runtime/scripts/lib/numeric-match.d.mts +2 -0
  800. package/runtime/scripts/lib/numeric-match.mjs +2 -2
  801. package/runtime/scripts/lib/package-at.d.mts +24 -0
  802. package/runtime/scripts/lib/package-at.mjs +34 -31
  803. package/runtime/scripts/lib/path-key.d.mts +76 -0
  804. package/runtime/scripts/lib/path-key.mjs +53 -38
  805. package/runtime/scripts/lib/pid-alive.d.mts +2 -0
  806. package/runtime/scripts/lib/pid-alive.mjs +10 -3
  807. package/runtime/scripts/lib/properties.d.mts +2 -0
  808. package/runtime/scripts/lib/properties.mjs +17 -11
  809. package/runtime/scripts/lib/provision-operation-schema.d.mts +4 -0
  810. package/runtime/scripts/lib/provision-operation-schema.mjs +4 -5
  811. package/runtime/scripts/lib/read-text.d.mts +12 -0
  812. package/runtime/scripts/lib/read-text.mjs +30 -17
  813. package/runtime/scripts/lib/redact.d.mts +52 -0
  814. package/runtime/scripts/lib/redact.mjs +135 -121
  815. package/runtime/scripts/lib/regex.d.mts +2 -0
  816. package/runtime/scripts/lib/regex.mjs +1 -2
  817. package/runtime/scripts/lib/roots.d.mts +32 -0
  818. package/runtime/scripts/lib/roots.mjs +5 -15
  819. package/runtime/scripts/lib/runtime-example.d.mts +1 -0
  820. package/runtime/scripts/lib/runtime-example.mjs +10 -8
  821. package/runtime/scripts/lib/same-text.d.mts +2 -0
  822. package/runtime/scripts/lib/same-text.mjs +0 -1
  823. package/runtime/scripts/lib/sealed-member.d.mts +16 -0
  824. package/runtime/scripts/lib/sealed-member.mjs +8 -13
  825. package/runtime/scripts/lib/secret-patterns.d.mts +30 -0
  826. package/runtime/scripts/lib/secret-patterns.mjs +175 -163
  827. package/runtime/scripts/lib/shell-source.d.mts +2 -0
  828. package/runtime/scripts/lib/shell-source.mjs +0 -1
  829. package/runtime/scripts/lib/sleep-sync.d.mts +2 -0
  830. package/runtime/scripts/lib/sleep-sync.mjs +5 -6
  831. package/runtime/scripts/lib/sops-envelope.d.mts +34 -0
  832. package/runtime/scripts/lib/sops-envelope.mjs +148 -120
  833. package/runtime/scripts/lib/source-phrases.d.mts +4 -0
  834. package/runtime/scripts/lib/source-phrases.mjs +13 -13
  835. package/runtime/scripts/lib/sql-ast.d.mts +13 -0
  836. package/runtime/scripts/lib/sql-ast.mjs +19 -18
  837. package/runtime/scripts/lib/sqlite.d.mts +21 -0
  838. package/runtime/scripts/lib/sqlite.mjs +3 -9
  839. package/runtime/scripts/lib/stack-declaration.d.mts +51 -0
  840. package/runtime/scripts/lib/stack-declaration.mjs +27 -20
  841. package/runtime/scripts/lib/stack-services.d.mts +73 -0
  842. package/runtime/scripts/lib/stack-services.mjs +128 -129
  843. package/runtime/scripts/lib/starciwork-boundary.d.mts +10 -0
  844. package/runtime/scripts/lib/starciwork-boundary.mjs +98 -92
  845. package/runtime/scripts/lib/swarm-stack.d.mts +24 -0
  846. package/runtime/scripts/lib/swarm-stack.mjs +112 -100
  847. package/runtime/scripts/lib/trim-end.d.mts +2 -0
  848. package/runtime/scripts/lib/trim-end.mjs +4 -4
  849. package/runtime/scripts/lib/ts-analysis.d.mts +24 -0
  850. package/runtime/scripts/lib/ts-analysis.mjs +28 -26
  851. package/runtime/scripts/lib/ts-ast.d.mts +36 -0
  852. package/runtime/scripts/lib/ts-ast.mjs +36 -42
  853. package/runtime/scripts/lib/walk.d.mts +19 -0
  854. package/runtime/scripts/lib/walk.mjs +33 -27
  855. package/runtime/scripts/lib/work-edition.d.mts +10 -0
  856. package/runtime/scripts/lib/work-edition.mjs +29 -21
  857. package/runtime/scripts/lib/workflow-events.d.mts +2 -0
  858. package/runtime/scripts/lib/workflow-events.mjs +5 -4
  859. package/runtime/scripts/lib/worktree-exclude.d.mts +16 -0
  860. package/runtime/scripts/lib/worktree-exclude.mjs +6 -8
  861. package/runtime/scripts/lib/yaml-cached.d.mts +2 -0
  862. package/runtime/scripts/lib/yaml-cached.mjs +4 -4
  863. package/runtime/scripts/machine/artifact-hold.d.mts +26 -0
  864. package/runtime/scripts/machine/artifact-hold.mjs +22 -23
  865. package/runtime/scripts/work/asset-slot.d.mts +96 -0
  866. package/runtime/scripts/work/asset-slot.mjs +201 -175
  867. package/runtime/scripts/work/draw/draw-dna-anatomy.d.mts +30 -0
  868. package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +40 -36
  869. package/runtime/scripts/work/draw/draw-dna-findings.d.mts +12 -0
  870. package/runtime/scripts/work/draw/draw-dna-findings.mjs +240 -206
  871. package/runtime/scripts/work/draw/draw-dna-selector.d.mts +7 -0
  872. package/runtime/scripts/work/draw/draw-dna-selector.mjs +28 -28
  873. package/runtime/scripts/work/draw/draw-dna.d.mts +191 -0
  874. package/runtime/scripts/work/draw/draw-dna.mjs +283 -249
  875. package/runtime/scripts/work/grammar-proposal.d.mts +83 -0
  876. package/runtime/scripts/work/grammar-proposal.mjs +215 -178
  877. package/runtime/scripts/work/work-io.d.mts +62 -0
  878. package/runtime/scripts/work/work-io.mjs +82 -63
  879. package/scaffold/add-app.d.mts +15 -0
  880. package/scaffold/add-app.mjs +111 -163
  881. package/scaffold/add-cli-lite.d.mts +8 -0
  882. package/scaffold/add-cli-lite.mjs +173 -207
  883. package/scaffold/add-compose.d.mts +44 -0
  884. package/scaffold/add-compose.mjs +62 -62
  885. package/scaffold/add-database-lite.d.mts +2 -0
  886. package/scaffold/add-database-lite.mjs +10 -10
  887. package/scaffold/add-fe-layout.d.mts +16 -0
  888. package/scaffold/add-fe-layout.mjs +23 -23
  889. package/scaffold/add-fe-overlay.d.mts +17 -0
  890. package/scaffold/add-fe-overlay.mjs +76 -78
  891. package/scaffold/add-fe-page.d.mts +41 -0
  892. package/scaffold/add-fe-page.mjs +84 -89
  893. package/scaffold/add-fe-unit.d.mts +59 -0
  894. package/scaffold/add-fe-unit.mjs +205 -197
  895. package/scaffold/add-inbox-lite.d.mts +17 -0
  896. package/scaffold/add-inbox-lite.mjs +209 -264
  897. package/scaffold/add-plan.d.mts +68 -0
  898. package/scaffold/add-plan.mjs +164 -149
  899. package/scaffold/add-queue-lite.d.mts +34 -0
  900. package/scaffold/add-queue-lite.mjs +267 -267
  901. package/scaffold/add-table.d.mts +16 -0
  902. package/scaffold/add-table.mjs +405 -442
  903. package/scaffold/add.d.mts +34 -0
  904. package/scaffold/add.mjs +260 -246
  905. package/scaffold/app.d.mts +198 -0
  906. package/scaffold/app.mjs +413 -391
  907. package/scaffold/edition-gate.d.mts +49 -0
  908. package/scaffold/edition-gate.mjs +19 -29
  909. package/scaffold/image.d.mts +4 -0
  910. package/scaffold/image.mjs +12 -12
  911. package/scaffold/lite-exports.d.mts +15 -0
  912. package/scaffold/lite-exports.mjs +262 -255
  913. package/scaffold/pattern-file-slots.d.mts +7 -0
  914. package/scaffold/pattern-file-slots.mjs +29 -24
  915. package/scaffold/service.d.mts +60 -0
  916. package/scaffold/service.mjs +297 -279
  917. package/scaffold/test-world.d.mts +31 -0
  918. package/scaffold/test-world.mjs +71 -78
  919. package/scaffold/yaml-block-edit.d.mts +23 -0
  920. package/scaffold/yaml-block-edit.mjs +95 -87
  921. package/scripts/sync-bundles.d.mts +31 -0
  922. package/src/fixtures/bypass-index.d.mts +4 -0
  923. package/src/fixtures/hfs-stack-tooling/passing.d.mts +8 -0
  924. package/src/fixtures/hfs-stack-tooling/violating.d.mts +19 -0
  925. package/src/fixtures/hfs-stacks-full/model-unavailable.d.mts +10 -0
  926. package/src/fixtures/hfs-stacks-full/passing.d.mts +19 -0
  927. package/src/fixtures/hfs-stacks-full/violating.d.mts +10 -0
  928. package/src/fixtures/loosening/adversarial.d.mts +4 -0
  929. package/src/fixtures/loosening/forms.d.mts +16 -0
  930. package/src/fixtures/loosening/swallows.d.mts +5 -0
  931. package/src/helpers/hfs-arch-fixture.d.mts +47 -0
  932. package/src/helpers/hfs-cli-fixture.d.mts +65 -0
  933. package/src/helpers/hfs-hfs-docker-rules-fixture.d.mts +5 -0
  934. package/src/helpers/hfs-secret-fixture.d.mts +19 -0
  935. package/src/helpers/pre-push-fixture.d.mts +21 -0
  936. package/src/helpers/stack-compose.d.mts +6 -0
  937. package/src/helpers/swarm-stack-fixture.d.mts +18 -0
  938. package/src/helpers/tmpdir.d.mts +2 -0
  939. package/src/main.d.mts +58 -0
  940. package/src/main.mjs +493 -348
  941. package/stack/age-install.d.mts +13 -0
  942. package/stack/age-install.mjs +28 -25
  943. package/stack/backup.d.mts +11 -0
  944. package/stack/backup.mjs +76 -69
  945. package/stack/build-args.d.mts +7 -0
  946. package/stack/build-args.mjs +22 -23
  947. package/stack/build.d.mts +8 -0
  948. package/stack/build.mjs +138 -119
  949. package/stack/cli.d.mts +55 -0
  950. package/stack/cli.mjs +187 -130
  951. package/stack/config.d.mts +197 -0
  952. package/stack/config.mjs +47 -33
  953. package/stack/context.d.mts +122 -0
  954. package/stack/context.mjs +63 -65
  955. package/stack/converge.d.mts +13 -0
  956. package/stack/converge.mjs +73 -69
  957. package/stack/deploy.d.mts +88 -0
  958. package/stack/deploy.mjs +272 -224
  959. package/stack/digest.d.mts +8 -0
  960. package/stack/digest.mjs +1 -6
  961. package/stack/docker-transport.d.mts +5 -0
  962. package/stack/docker-transport.mjs +101 -93
  963. package/stack/edge-alias.d.mts +42 -0
  964. package/stack/edge-alias.mjs +62 -60
  965. package/stack/errors.d.mts +18 -0
  966. package/stack/errors.mjs +8 -10
  967. package/stack/image-scan.d.mts +20 -0
  968. package/stack/image-scan.mjs +114 -101
  969. package/stack/ingress-create.d.mts +4 -0
  970. package/stack/ingress-create.mjs +82 -86
  971. package/stack/ingress-publish.d.mts +6 -0
  972. package/stack/ingress-publish.mjs +194 -182
  973. package/stack/ingress.d.mts +55 -0
  974. package/stack/ingress.mjs +534 -524
  975. package/stack/jobs.d.mts +12 -0
  976. package/stack/jobs.mjs +35 -34
  977. package/stack/live-restore.d.mts +35 -0
  978. package/stack/live-restore.mjs +120 -125
  979. package/stack/post-deploy.d.mts +8 -0
  980. package/stack/post-deploy.mjs +25 -25
  981. package/stack/prepare.d.mts +24 -0
  982. package/stack/prepare.mjs +213 -200
  983. package/stack/private-file.d.mts +5 -0
  984. package/stack/private-file.mjs +9 -9
  985. package/stack/provision.d.mts +25 -0
  986. package/stack/provision.mjs +64 -63
  987. package/stack/prune.d.mts +11 -0
  988. package/stack/prune.mjs +38 -27
  989. package/stack/release-store.d.mts +28 -0
  990. package/stack/release-store.mjs +68 -67
  991. package/stack/remote.d.mts +83 -0
  992. package/stack/remote.mjs +176 -162
  993. package/stack/render.d.mts +14 -0
  994. package/stack/render.mjs +132 -124
  995. package/stack/retire.d.mts +9 -0
  996. package/stack/retire.mjs +31 -34
  997. package/stack/sealed.d.mts +46 -0
  998. package/stack/sealed.mjs +78 -82
  999. package/stack/server-key.d.mts +67 -0
  1000. package/stack/server-key.mjs +217 -203
  1001. package/stack/ssh-session.d.mts +29 -0
  1002. package/stack/ssh-session.mjs +133 -123
  1003. package/stack/target-binding.d.mts +21 -0
  1004. package/stack/target-binding.mjs +68 -44
  1005. package/stack/task.d.mts +12 -0
  1006. package/stack/task.mjs +26 -31
  1007. package/stack/urgent-floor.d.mts +106 -0
  1008. package/stack/urgent-floor.mjs +211 -160
  1009. package/stack/urgent-release.d.mts +38 -0
  1010. package/stack/urgent-release.mjs +26 -22
  1011. package/stack/verify.d.mts +14 -0
  1012. package/stack/verify.mjs +75 -65
  1013. package/sync/ci-commands.d.mts +26 -0
  1014. package/sync/ci-commands.mjs +181 -182
  1015. package/sync/cli.d.mts +8 -0
  1016. package/sync/cli.mjs +9 -9
  1017. package/sync/format.d.mts +38 -0
  1018. package/sync/format.mjs +30 -24
  1019. package/sync/hygiene.d.mts +23 -0
  1020. package/sync/hygiene.mjs +113 -99
  1021. package/sync/index.d.mts +237 -0
  1022. package/sync/index.mjs +457 -436
  1023. package/sync/managed.d.mts +23 -0
  1024. package/sync/managed.mjs +104 -94
  1025. package/sync/quality-services.d.mts +35 -0
  1026. package/sync/quality-services.mjs +48 -41
  1027. package/sync/readme-badges.d.mts +13 -0
  1028. package/sync/readme-badges.mjs +45 -44
  1029. package/sync/sonar-key.d.mts +31 -0
  1030. package/sync/sonar-key.mjs +29 -28
  1031. package/sync/stack-images.d.mts +36 -0
  1032. package/sync/stack-images.mjs +85 -95
  1033. package/sync/ts-strict.d.mts +14 -0
  1034. package/sync/ts-strict.mjs +43 -34
  1035. package/templates/app/ci-workflows/github/workflows/ci.yml +1 -1
  1036. package/templates/app/ci-workflows/github/workflows/images.yml +1 -1
  1037. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +2 -2
  1038. package/templates/app/ci-workflows/github/workflows/stack-key-restore.yml +1 -1
  1039. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -1
  1040. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  1041. package/templates/app/ci-workflows-lite/github/workflows/images.yml +1 -1
  1042. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +2 -2
  1043. package/templates/app/ci-workflows-lite/github/workflows/stack-key-restore.yml +1 -1
  1044. package/templates/app/codegen-lite/scripts/codegen.mjs +1 -1
  1045. package/templates/app/db-bootstrap/db/bootstrap.sql +1 -1
  1046. package/templates/app/db-image/starcistacks/images/db/Dockerfile +1 -1
  1047. package/templates/app/hooks/husky/pre-commit +2 -2
  1048. package/templates/app/package-scripts/db.json +3 -3
  1049. package/templates/app/package-scripts/package.json +1 -1
  1050. package/templates/app/quality-config/sonar-project.properties +1 -1
  1051. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +2 -2
  1052. package/templates/app/skeleton/.starcistacks/production/README.md +4 -4
  1053. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +8 -3
  1054. package/templates/app/skeleton/README.md +1 -1
  1055. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +2 -2
  1056. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +1 -1
  1057. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +6 -2
  1058. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +3 -2
  1059. package/templates/app/skeleton-lite/.starcistacks/production/README.md +5 -5
  1060. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +7 -2
  1061. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +3 -2
  1062. package/templates/app/skeleton-lite/README.md +2 -2
  1063. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +1 -1
  1064. package/templates/be/skeleton/apps/cli/src/main.ts +6 -4
  1065. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +12 -0
  1066. package/templates/be/skeleton/src/modules/platform/database/abstract.entity.ts +19 -0
  1067. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  1068. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +19 -0
  1069. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +4 -3
  1070. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +1 -1
  1071. package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +5 -2
  1072. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +1 -1
  1073. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.spec.ts +40 -0
  1074. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +1 -0
  1075. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.spec.ts +48 -1
  1076. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +5 -0
  1077. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +5 -0
  1078. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +1 -1
  1079. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +7 -3
  1080. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +7 -1
  1081. package/templates/be/table/entity.ts.tpl +4 -15
  1082. package/upgrade/check.d.mts +9 -0
  1083. package/upgrade/check.mjs +8 -8
  1084. package/upgrade/file-state.d.mts +6 -0
  1085. package/upgrade/file-state.mjs +32 -40
  1086. package/upgrade/index.d.mts +13 -0
  1087. package/upgrade/index.mjs +122 -151
  1088. package/vendor-types.d.mts +8 -0
@@ -37,29 +37,32 @@ import path from 'node:path';
37
37
  import crypto from 'node:crypto';
38
38
  import { createRequire } from 'node:module';
39
39
  import { pathToFileURL, fileURLToPath } from 'node:url';
40
- import { putBlob as storeBlob, blobPath, getBlob } from './blob.mjs'; import { EVENT_LIMITS } from './event-compact.mjs';
41
- import { redactBytes, redactData, redactText } from '../../scripts/lib/redact.mjs'; import { isMain } from '../../scripts/lib/is-main.mjs';
40
+ import { putBlob as storeBlob, blobPath, getBlob } from './blob.mjs';
41
+ import { EVENT_LIMITS } from './event-compact.mjs';
42
+ import { redactBytes, redactData, redactText } from '../../scripts/lib/redact.mjs';
43
+ import { isMain } from '../../scripts/lib/is-main.mjs';
42
44
  import { isSpecRun, readEnv } from '../../scripts/lib/env.mjs';
43
- import { pathKey } from '../../scripts/lib/path-key.mjs'; import { pidAlive } from '../../scripts/lib/pid-alive.mjs'; import { releaseObservedProvisionLock, renewProvisionLock, releaseProvisionLock } from './provision-lock.mjs';
45
+ import { pathKey } from '../../scripts/lib/path-key.mjs';
46
+ import { pidAlive } from '../../scripts/lib/pid-alive.mjs';
47
+ import { releaseObservedProvisionLock, renewProvisionLock, releaseProvisionLock } from './provision-lock.mjs';
44
48
  import { insertRowWith } from '../../scripts/lib/sqlite.mjs';
45
49
  import { need as refuseUnless } from '../refuse.mjs';
46
50
  import { sha256 } from '../digest.mjs';
47
51
  import { starciLocalRoot } from '../runtime-root.mjs';
48
52
  import { machineSchemaMethods } from './machine-schema.mjs';
49
- import { machineConnectionMethods, corruptDiagnostic, MACHINE_BUSY_TIMEOUT_MS, MACHINE_CORRUPT_CODE, CORRUPT_RETRY_DELAYS_MS,
50
- waitForRetry, isCorruptError, errText } from './machine-connection.mjs';
53
+ import { machineConnectionMethods, corruptDiagnostic, MACHINE_BUSY_TIMEOUT_MS, MACHINE_CORRUPT_CODE, CORRUPT_RETRY_DELAYS_MS, waitForRetry, isCorruptError, errText } from './machine-connection.mjs';
51
54
  export { MACHINE_BUSY_CODE, isMachineBusy, isBusyError } from './machine-connection.mjs';
52
55
  import { providerReservationMethods } from './provider-reservations.mjs';
53
56
  import { tempRoot } from '../temp-root.mjs';
54
-
55
57
  const require = createRequire(import.meta.url);
58
+ /** The reads an observer binds. */
59
+ const OBSERVER_READS = () => ({ listLedgers, services, seats, providerHealth, logs, latestMetrics, throttleState, poolBackoff, quotas, hostLeases, budgets });
56
60
  const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
57
61
  export const MACHINE_SCHEMA = 'starci/machine@1';
58
62
  export const MACHINE_VERSION = 3;
59
63
  /** Test seam: STARCI_MACHINE_BUSY_TIMEOUT_MS (a positive integer) replaces the writer's busy_timeout; unset in production. */
60
64
  export const INIT_SQL_FILE = path.join(ENGINE_DIR, 'schema', 'machine.sql');
61
65
  export const CONTROLLERS = Object.freeze(['job', 'workflow', 'resource', 'host', 'gc', 'workers', 'learning']);
62
-
63
66
  // ---------------------------------------------------------------------------------------------------------------------
64
67
  // Paths
65
68
  // ---------------------------------------------------------------------------------------------------------------------
@@ -75,66 +78,97 @@ export { LOCAL_ROOT_ENV, starciLocalRoot } from '../runtime-root.mjs';
75
78
  export const localProjectsRoot = (env = process.env) => path.join(starciLocalRoot(env), 'projects');
76
79
  /** The runtime.sqlite of one ledger (decision Q1): <runtime root>/.runtime/projects/<ledger_id>/runtime.sqlite. */
77
80
  const projectLedgerFile = (ledgerId, env = process.env) => {
78
- if (!/^[A-Za-z0-9-]{8,64}$/.test(String(ledgerId ?? ''))) throw new Error(`projectLedgerFile needs a ledger id, got ${ledgerId}`);
79
- return path.join(localProjectsRoot(env), String(ledgerId), 'runtime.sqlite');
81
+ if (!/^[A-Za-z0-9-]{8,64}$/.test(String(ledgerId ?? '')))
82
+ throw new Error(`projectLedgerFile needs a ledger id, got ${ledgerId}`);
83
+ return path.join(localProjectsRoot(env), String(ledgerId), 'runtime.sqlite');
80
84
  };
81
85
  /** The explicit test registry: a machine.sqlite that replaces the host's for this process tree (e2e/setup/isolated-registry.mjs). */
82
86
  export const TEST_REGISTRY_ENV = 'STARCI_TEST_MACHINE_FILE';
83
87
  const normDir = (file) => pathKey(file, { fold: true });
84
- const tempDirsOf = (env = process.env) => [...new Set([os.tmpdir(), tempRoot({ env }), env.TEMP, env.TMP].filter(Boolean)
85
- .flatMap((dir) => { const out = [normDir(dir)]; try { out.push(normDir(fs.realpathSync.native(dir))); } catch { /* missing */ } return out; }))].filter((dir) => !/^(?:[a-z]:)?$/.test(dir));
88
+ const tempDirsOf = (env = process.env) => [...new Set([os.tmpdir(), tempRoot({ env }), env.TEMP, env.TMP].filter((dir) => Boolean(dir))
89
+ .flatMap((dir) => { const out = [normDir(dir)]; try {
90
+ out.push(normDir(fs.realpathSync.native(dir)));
91
+ }
92
+ catch { /* missing */ } return out; }))].filter((dir) => !/^(?:[a-z]:)?$/.test(dir));
86
93
  /** True when `file` sits under the OS temp directory or the configured temp root, as written or as its realpath. */
87
94
  export function isUnderTempDir(file, { env = process.env, tempDirs = tempDirsOf(env) } = {}) {
88
- if (typeof file !== 'string' || !file) return false;
89
- const forms = [normDir(file)];
90
- try { forms.push(normDir(fs.realpathSync.native(file))); } catch { /* missing */ }
91
- return forms.some((form) => tempDirs.map(normDir).some((dir) => form.startsWith(`${dir}/`)));
95
+ if (typeof file !== 'string' || !file)
96
+ return false;
97
+ const forms = [normDir(file)];
98
+ try {
99
+ forms.push(normDir(fs.realpathSync.native(file)));
100
+ }
101
+ catch { /* missing */ }
102
+ return forms.some((form) => tempDirs.map(normDir).some((dir) => form.startsWith(`${dir}/`)));
92
103
  }
93
104
  /**
94
105
  * machine.sqlite for `env`: TEST_REGISTRY_ENV when set; else <starciLocalRoot>/machine.sqlite (beside projects/) — except inside a node --test
95
106
  * process tree whose runtime root is not under the temp directory, which gets a shared temp registry instead.
96
107
  */
97
108
  export const machineFileFor = (env = process.env) => {
98
- if (env[TEST_REGISTRY_ENV]) return path.resolve(env[TEST_REGISTRY_ENV]);
99
- const file = path.join(starciLocalRoot(env), 'machine.sqlite');
100
- if (isSpecRun(env) && !isUnderTempDir(file, { env })) return path.join(tempRoot({ env }), 'starci-test-registry', 'machine.sqlite');
101
- return file;
109
+ if (env[TEST_REGISTRY_ENV])
110
+ return path.resolve(env[TEST_REGISTRY_ENV]);
111
+ const file = path.join(starciLocalRoot(env), 'machine.sqlite');
112
+ if (isSpecRun(env) && !isUnderTempDir(file, { env }))
113
+ return path.join(tempRoot({ env }), 'starci-test-registry', 'machine.sqlite');
114
+ return file;
102
115
  };
103
-
104
116
  // ---------------------------------------------------------------------------------------------------------------------
105
117
  // Small helpers
106
118
  // ---------------------------------------------------------------------------------------------------------------------
107
- const need = (ok, message, code = 'STARCI_MACHINE_DB') => refuseUnless(ok, message, code);
119
+ function need(ok, message, code = 'STARCI_MACHINE_DB') { refuseUnless(ok, message, code); }
108
120
  const hex = (bytes) => crypto.randomBytes(bytes).toString('hex');
109
121
  export const newTraceId = () => hex(16);
110
122
  export const newSpanId = () => hex(8);
111
123
  const JSON_LIMIT = 65536;
112
- const toJson = (value) => { if (value === undefined || value === null) { return null; } if (typeof value !== 'string') { return JSON.stringify(value); } JSON.parse(value); return value; };
113
- const parse = (text) => { if (text == null) { return null; } try { return JSON.parse(text); } catch { return null; } };
124
+ const toJson = (value) => { if (value === undefined || value === null) {
125
+ return null;
126
+ } if (typeof value !== 'string') {
127
+ return JSON.stringify(value);
128
+ } JSON.parse(value); return value; };
129
+ const parse = (text) => { if (text == null) {
130
+ return null;
131
+ } try {
132
+ return JSON.parse(text);
133
+ }
134
+ catch {
135
+ return null;
136
+ } };
114
137
  const int = (v) => (v === undefined || v === null || v === '' ? null : Math.trunc(Number(v)));
115
- const bool = (v) => { if (v === undefined || v === null) { return null; } return v ? 1 : 0; };
138
+ const bool = (v) => { if (v === undefined || v === null) {
139
+ return null;
140
+ } return v ? 1 : 0; };
116
141
  export { pidAlive };
117
142
  const { openWithRetry, connectionState } = machineConnectionMethods({ reportIncident: corruptIncident });
118
143
  /** Operational failures also persist their incident through a fresh writer or the deferred outbox. */
119
144
  function corruptIncident(file, error, details) {
120
- const out = corruptDiagnostic(file, error, details);
121
- if (out === error) return out;
122
- const { retries, where } = details;
123
- const row = { actor: 'harness', kind: 'machine-db.corrupt', level: 'error', msg: out.message,
124
- data: { file, where, retries, error: errText(error), errcode: error?.errcode ?? null, quickCheck: out.quickCheck, pid: process.pid, sqlite: process.versions.sqlite, node: process.version } };
125
- try {
126
- const { DatabaseSync } = require('node:sqlite');
127
- const db = new DatabaseSync(file, { timeout: MACHINE_BUSY_TIMEOUT_MS });
145
+ const out = corruptDiagnostic(file, error, details);
146
+ if (out === error)
147
+ return out;
148
+ const { retries, where } = details;
149
+ const row = { actor: 'harness', kind: 'machine-db.corrupt', level: 'error', msg: out.message,
150
+ data: { file, where, retries, error: errText(error), errcode: error?.errcode ?? null, quickCheck: out.quickCheck, pid: process.pid, sqlite: process.versions.sqlite, node: process.version } };
128
151
  try {
129
- db.exec('PRAGMA wal_autocheckpoint=0;');
130
- db.prepare('INSERT INTO machine_logs(at,actor,level,kind,msg,data_json) VALUES(?,?,?,?,?,?)').run(Date.now(), row.actor, row.level, row.kind, row.msg, JSON.stringify(row.data));
131
- } finally { db.close(); }
132
- } catch (e) {
133
- try { out.deferred = deferWrite({ op: 'log', args: [{ ...row, src: null }], file, error: e }); } catch (error_) { process.stderr.write(`[machine-db] INCIDENT could not be recorded: ${errText(error_)}\n`); }
134
- }
135
- return out;
136
- }
137
-
152
+ const { DatabaseSync } = require('node:sqlite');
153
+ const db = new DatabaseSync(file, { timeout: MACHINE_BUSY_TIMEOUT_MS });
154
+ try {
155
+ db.exec('PRAGMA wal_autocheckpoint=0;');
156
+ db.prepare('INSERT INTO machine_logs(at,actor,level,kind,msg,data_json) VALUES(?,?,?,?,?,?)').run(Date.now(), row.actor, row.level, row.kind, row.msg, JSON.stringify(row.data));
157
+ }
158
+ finally {
159
+ db.close();
160
+ }
161
+ }
162
+ catch (e) {
163
+ try {
164
+ out.deferred = deferWrite({ op: 'log', args: [{ ...row, src: null }], file, error: e });
165
+ }
166
+ catch (error_) {
167
+ process.stderr.write(`[machine-db] INCIDENT could not be recorded: ${errText(error_)}\n`);
168
+ }
169
+ }
170
+ return out;
171
+ }
138
172
  /**
139
173
  * The rev of the runtime this process runs, as its callers know it: '<HEAD committer time, ms, 13 digits>:<short sha>' of
140
174
  * the harness checkout, which the caller computes and passes in (`rev`,
@@ -143,57 +177,63 @@ function corruptIncident(file, error, details) {
143
177
  * 'unknown' sorts before every real rev.
144
178
  */
145
179
  export function runtimeRev() {
146
- return readEnv('STARCI_RUNTIME_REV') ? String(readEnv('STARCI_RUNTIME_REV')) : 'unknown';
180
+ return readEnv('STARCI_RUNTIME_REV') ? String(readEnv('STARCI_RUNTIME_REV')) : 'unknown';
147
181
  }
148
182
  /** Order of two runtime revs: <0 when a is older than b (time prefix; anything unparsable is oldest). */
149
183
  const compareRevs = (a, b) => { const t = (r) => (/^\d{13}:/.test(String(r ?? '')) ? Number(String(r).slice(0, 13)) : -1); return t(a) - t(b); };
150
-
151
184
  const { checkSchema, createSchema } = machineSchemaMethods({ schema: MACHINE_SCHEMA, version: MACHINE_VERSION, initSqlFile: INIT_SQL_FILE, controllers: CONTROLLERS, runtimeRev, pragma });
152
185
  const { openConnection } = machineOpenMethods({ checkSchema, createSchema, openWithRetry, corruptIncident });
153
-
154
186
  // ---------------------------------------------------------------------------------------------------------------------
155
187
  // Generic row writes (column names are checked against the live table; table names are this module's constants)
156
188
  // ---------------------------------------------------------------------------------------------------------------------
157
189
  const columnsCache = new WeakMap();
158
190
  function columnsOf(db, table) {
159
- let byTable = columnsCache.get(db);
160
- if (!byTable) { byTable = new Map(); columnsCache.set(db, byTable); }
161
- if (!byTable.has(table)) {
162
- const cols = db.prepare(`PRAGMA table_xinfo(${JSON.stringify(table)})`).all().filter((c) => !c.hidden).map((c) => c.name);
163
- need(cols.length, `machine-db: unknown table ${table}`);
164
- byTable.set(table, new Set(cols));
165
- }
166
- return byTable.get(table);
191
+ let byTable = columnsCache.get(db);
192
+ if (!byTable) {
193
+ byTable = new Map();
194
+ columnsCache.set(db, byTable);
195
+ }
196
+ if (!byTable.has(table)) {
197
+ const cols = db.prepare(`PRAGMA table_xinfo(${JSON.stringify(table)})`).all().filter((c) => !c.hidden).map((c) => c.name);
198
+ need(cols.length, `machine-db: unknown table ${table}`);
199
+ byTable.set(table, new Set(cols));
200
+ }
201
+ return byTable.get(table);
167
202
  }
168
203
  const cellOf = (key, value) => {
169
- if (value === undefined) return undefined;
170
- if (value === null) return null;
171
- if (key.endsWith('_json')) return toJson(value);
172
- if (typeof value === 'boolean') return value ? 1 : 0;
173
- if (typeof value === 'object') return JSON.stringify(value);
174
- return value;
204
+ if (value === undefined)
205
+ return undefined;
206
+ if (value === null)
207
+ return null;
208
+ if (key.endsWith('_json'))
209
+ return toJson(value);
210
+ if (typeof value === 'boolean')
211
+ return value ? 1 : 0;
212
+ if (typeof value === 'object')
213
+ return JSON.stringify(value);
214
+ return value;
175
215
  };
176
216
  function rowCells(db, table, row) {
177
- const cols = columnsOf(db, table);
178
- const entries = Object.entries(row).map(([k, v]) => [k, cellOf(k, v)]).filter(([, v]) => v !== undefined);
179
- for (const [k] of entries) need(cols.has(k), `machine-db: ${table} has no column ${k}`);
180
- return entries;
217
+ const cols = columnsOf(db, table);
218
+ const entries = Object.entries(row).map(([k, v]) => [k, cellOf(k, v)]).filter(([, v]) => v !== undefined);
219
+ for (const [k] of entries)
220
+ need(cols.has(k), `machine-db: ${table} has no column ${k}`);
221
+ return entries;
181
222
  }
182
223
  const insertRow = insertRowWith(rowCells);
183
224
  function upsertRow(db, table, row, keys) {
184
- const entries = rowCells(db, table, row);
185
- const updates = entries.filter(([k]) => !keys.includes(k));
186
- const sql = `INSERT INTO ${table}(${entries.map(([k]) => k).join(',')}) VALUES(${entries.map(() => '?').join(',')}) ON CONFLICT(${keys.join(',')}) DO `
187
- + (updates.length ? `UPDATE SET ${updates.map(([k]) => k + '=excluded.' + k).join(',')}` : 'NOTHING');
188
- return db.prepare(sql).run(...entries.map(([, v]) => v));
225
+ const entries = rowCells(db, table, row);
226
+ const updates = entries.filter(([k]) => !keys.includes(k));
227
+ const sql = `INSERT INTO ${table}(${entries.map(([k]) => k).join(',')}) VALUES(${entries.map(() => '?').join(',')}) ON CONFLICT(${keys.join(',')}) DO `
228
+ + (updates.length ? `UPDATE SET ${updates.map(([k]) => k + '=excluded.' + k).join(',')}` : 'NOTHING');
229
+ return db.prepare(sql).run(...entries.map(([, v]) => v));
189
230
  }
190
231
  function updateRow(db, table, set, where) {
191
- const s = rowCells(db, table, set), w = rowCells(db, table, where);
192
- need(s.length && w.length, `machine-db: update ${table} needs set and where`);
193
- return db.prepare(`UPDATE ${table} SET ${s.map(([k]) => k + '=?').join(',')} WHERE ${w.map(([k]) => k + ' IS ?').join(' AND ')}`)
194
- .run(...s.map(([, v]) => v), ...w.map(([, v]) => v));
232
+ const s = rowCells(db, table, set), w = rowCells(db, table, where);
233
+ need(s.length && w.length, `machine-db: update ${table} needs set and where`);
234
+ return db.prepare(`UPDATE ${table} SET ${s.map(([k]) => k + '=?').join(',')} WHERE ${w.map(([k]) => k + ' IS ?').join(' AND ')}`)
235
+ .run(...s.map(([, v]) => v), ...w.map(([, v]) => v));
195
236
  }
196
-
197
237
  // ---------------------------------------------------------------------------------------------------------------------
198
238
  // The handle
199
239
  // ---------------------------------------------------------------------------------------------------------------------
@@ -203,173 +243,263 @@ function updateRow(db, table, set, where) {
203
243
  * the engine_leader row names it (header).
204
244
  */
205
245
  export function openMachine({ file = null, env = process.env, now = Date.now, checkpointer = false, tempDirs = null } = {}) {
206
- const resolved = path.resolve(file ?? machineFileFor(env));
207
- const live = !env[TEST_REGISTRY_ENV] && !isUnderTempDir(resolved, { env, tempDirs: tempDirs ?? tempDirsOf(env) });
208
- return makeHandle(() => openConnection(resolved, { env, now }), { file: resolved, env, now, live, readOnly: false, checkpointer, tempDirs: tempDirs ?? tempDirsOf(env) });
246
+ const resolved = path.resolve(file ?? machineFileFor(env));
247
+ const live = !env[TEST_REGISTRY_ENV] && !isUnderTempDir(resolved, { env, tempDirs: tempDirs ?? tempDirsOf(env) });
248
+ return makeHandle(() => openConnection(resolved, { env, now }), { file: resolved, env, now, live, readOnly: false, checkpointer, tempDirs: tempDirs ?? tempDirsOf(env) });
209
249
  }
210
250
  /** Open machine.sqlite read-only (query_only); null when the file does not exist yet. */
211
251
  export function openMachineReader({ file = null, env = process.env, now = Date.now } = {}) {
212
- const resolved = path.resolve(file ?? machineFileFor(env));
213
- if (!fs.existsSync(resolved)) return null;
214
- return makeHandle(() => openConnection(resolved, { readOnly: true }), { file: resolved, env, now, live: false, readOnly: true, checkpointer: false, tempDirs: [] });
252
+ const resolved = path.resolve(file ?? machineFileFor(env));
253
+ if (!fs.existsSync(resolved))
254
+ return null;
255
+ return makeHandle(() => openConnection(resolved, { readOnly: true }), { file: resolved, env, now, live: false, readOnly: true, checkpointer: false, tempDirs: [] });
215
256
  }
216
257
  /** Observation-only reads: supported schemas, bounded retries and diagnostics without incident or recovery writes. */
217
258
  export function openMachineObserver({ file = null, env = process.env, now = Date.now } = {}) {
218
- const resolved = path.resolve(file ?? machineFileFor(env));
219
- if (!fs.existsSync(resolved)) return null;
220
- const { db, recovered, close } = connectionState(() => openConnection(resolved, { readOnly: true, env, now, onCorrupt: corruptDiagnostic }),
221
- { file: resolved, onCorrupt: corruptDiagnostic });
222
- const observer = { schema: MACHINE_SCHEMA, file: resolved, path: resolved, readOnly: true, db, recovered, close };
223
- for (const [name, fn] of Object.entries({ listLedgers, services, seats, providerHealth, logs, latestMetrics, throttleState, poolBackoff, quotas, hostLeases, budgets }))
224
- observer[name] = (...args) => fn(observer, ...args);
225
- return observer;
259
+ const resolved = path.resolve(file ?? machineFileFor(env));
260
+ if (!fs.existsSync(resolved))
261
+ return null;
262
+ const { db, recovered, close } = connectionState(() => openConnection(resolved, { readOnly: true, env, now, onCorrupt: corruptDiagnostic }), { file: resolved, onCorrupt: corruptDiagnostic });
263
+ const observer = { schema: MACHINE_SCHEMA, file: resolved, path: resolved, readOnly: true, db, recovered, close }; // the reads are bound just below
264
+ for (const [name, fn] of Object.entries(OBSERVER_READS()))
265
+ observer[name] = (...args) => fn(observer, ...args);
266
+ return observer;
226
267
  }
227
268
  /** fn(handle) over a writer, closed afterwards. */
228
269
  export function withMachine(fn, options = {}) {
229
- const m = openMachine(options);
230
- try { return fn(m); } finally { m.close(); }
270
+ const m = openMachine(options);
271
+ try {
272
+ return fn(m);
273
+ }
274
+ finally {
275
+ m.close();
276
+ }
231
277
  }
232
- /** fn(handle) over a reader; `fallback` when the store does not exist or cannot be read — but a corrupt store is thrown, never hidden. */
233
278
  export function readMachine(fn, fallback = null, options = {}) {
234
- let m = null;
235
- try { m = openMachineReader(options); return m ? fn(m) : fallback; }
236
- catch (error) { if (isCorruptError(error)) { throw error; } return fallback; }
237
- finally { try { m?.close(); } catch { /* closed */ } }
279
+ let m = null;
280
+ try {
281
+ m = openMachineReader(options);
282
+ return m ? fn(m) : fallback;
283
+ }
284
+ catch (error) {
285
+ if (isCorruptError(error)) {
286
+ throw error;
287
+ }
288
+ return fallback;
289
+ }
290
+ finally {
291
+ try {
292
+ m?.close();
293
+ }
294
+ catch { /* closed */ }
295
+ }
238
296
  }
239
-
240
297
  // The fenced unit of a transaction(): COMMIT or ROLLBACK-and-rethrow.
241
- const commitUnit = (db, fn) => { try { assertMutationFence({ kind: 'machine-write', db }); const out = fn(db); db.exec('COMMIT'); return out; } catch (error) { try { if (db.isTransaction) db.raw.exec('ROLLBACK'); } catch { /* none */ } throw error; } };
298
+ const commitUnit = (db, fn) => { try {
299
+ assertMutationFence({ kind: 'machine-write', db });
300
+ const out = fn(db);
301
+ db.exec('COMMIT');
302
+ return out;
303
+ }
304
+ catch (error) {
305
+ try {
306
+ if (db.isTransaction)
307
+ db.raw.exec('ROLLBACK');
308
+ }
309
+ catch { /* none */ }
310
+ throw error;
311
+ } };
242
312
  // transaction()'s catch: a transient corrupt error waits, reopens and retries; anything else (or retries spent) throws.
243
- const retryCorrupt = (error, { file, retries, db }) => { if (!isCorruptError(error) || error.code === MACHINE_CORRUPT_CODE) { throw error; }
244
- if (retries >= CORRUPT_RETRY_DELAYS_MS.length) { throw corruptIncident(file, error, { retries, where: 'transaction' }); }
245
- waitForRetry(CORRUPT_RETRY_DELAYS_MS[retries]); try { db.reopen(); } catch (openError) { if (!isCorruptError(openError)) { throw openError; } } };
246
- function makeHandle(openRaw, { file, env, now, live, readOnly, checkpointer, tempDirs }) {
247
- let depth = 0;
248
- const { db, recovered, noteRecovered, close } = connectionState(openRaw, { file, inTransaction: () => depth > 0 });
249
- /**
250
- * BEGIN IMMEDIATE … COMMIT (nested calls join the open one). A transient corrupt error anywhere in the unit rolls it
251
- * back, reopens the connection and runs the unit again (bodies are DB-only, RESEARCH-STORAGE §3 rule 3, so a re-run is
252
- * exact); still corrupt after CORRUPT_RETRY_DELAYS_MS it is an incident.
253
- */
254
- const transaction = (fn) => {
255
- if (depth > 0) return fn(db); // nested: join the open transaction
256
- need(!readOnly, 'machine-db: read-only handle');
257
- for (let retries = 0; ; retries += 1) {
258
- try {
259
- db.exec('BEGIN IMMEDIATE'); // a refused BEGIN changed nothing: db.exec backs off (BUSY_RETRY_DELAYS_MS), then throws STARCI_MACHINE_BUSY
260
- depth += 1;
261
- let out;
262
- try { out = commitUnit(db, fn); } finally { depth -= 1; }
263
- if (retries) noteRecovered({ where: 'transaction', retries });
264
- return out;
265
- } catch (error) { retryCorrupt(error, { file, retries, db }); }
313
+ const retryCorrupt = (error, { file, retries, db }) => {
314
+ if (!isCorruptError(error) || error.code === MACHINE_CORRUPT_CODE) {
315
+ throw error;
266
316
  }
267
- };
268
- const m = { schema: MACHINE_SCHEMA, file, path: file, db, env, now, live, readOnly, checkpointer: Boolean(checkpointer) && !readOnly, transaction,
269
- /** Retries that recovered on this handle ({where, retries, at}); close() records them as one machine_logs warn row. */
270
- recovered,
271
- close() {
272
- if (recovered.length) {
273
- const row = { actor: 'harness', kind: 'machine-db.corrupt-recovered', level: 'warn', msg: `transient SQLITE_CORRUPT recovered ${recovered.length} time(s) on ${path.basename(file)}`,
274
- data: { file, pid: process.pid, recovered: recovered.slice(0, 20), sqlite: process.versions.sqlite, node: process.version } };
275
- recovered.length = 0;
276
- // a reader cannot write: its notice waits in the outbox for the next flush
277
- try { if (readOnly) { throw new Error('read-only handle'); } API.log(m, row); } catch (error) { try { deferWrite({ op: 'log', args: [row], file, error }); } catch { /* stderr already has it */ } }
278
- }
279
- close();
280
- } };
281
- for (const [name, fn] of Object.entries(API)) m[name] = (...args) => {
282
- if (!readOnly) assertMutationFence({ kind: 'machine-access', name });
283
- return fn(m, ...args);
284
- };
285
- m.tempDirs = tempDirs;
286
- return m;
287
- }
288
-
317
+ if (retries >= CORRUPT_RETRY_DELAYS_MS.length) {
318
+ throw corruptIncident(file, error, { retries, where: 'transaction' });
319
+ }
320
+ waitForRetry(CORRUPT_RETRY_DELAYS_MS[retries]);
321
+ try {
322
+ db.reopen();
323
+ }
324
+ catch (openError) {
325
+ if (!isCorruptError(openError)) {
326
+ throw openError;
327
+ }
328
+ }
329
+ };
330
+ function makeHandle(openRaw, { file, env, now, live, readOnly, checkpointer, tempDirs }) {
331
+ let depth = 0;
332
+ const { db, recovered, noteRecovered, close } = connectionState(openRaw, { file, inTransaction: () => depth > 0 });
333
+ /**
334
+ * BEGIN IMMEDIATE … COMMIT (nested calls join the open one). A transient corrupt error anywhere in the unit rolls it
335
+ * back, reopens the connection and runs the unit again (bodies are DB-only, RESEARCH-STORAGE §3 rule 3, so a re-run is
336
+ * exact); still corrupt after CORRUPT_RETRY_DELAYS_MS it is an incident.
337
+ */
338
+ const transaction = (fn) => {
339
+ if (depth > 0)
340
+ return fn(db); // nested: join the open transaction
341
+ need(!readOnly, 'machine-db: read-only handle');
342
+ for (let retries = 0;; retries += 1) {
343
+ try {
344
+ db.exec('BEGIN IMMEDIATE'); // a refused BEGIN changed nothing: db.exec backs off (BUSY_RETRY_DELAYS_MS), then throws STARCI_MACHINE_BUSY
345
+ depth += 1;
346
+ let out;
347
+ try {
348
+ out = commitUnit(db, fn);
349
+ }
350
+ finally {
351
+ depth -= 1;
352
+ }
353
+ if (retries)
354
+ noteRecovered({ where: 'transaction', retries });
355
+ return out;
356
+ }
357
+ catch (error) {
358
+ retryCorrupt(error, { file, retries, db });
359
+ }
360
+ }
361
+ };
362
+ const core = { schema: MACHINE_SCHEMA, file, path: file, db, env, now, live, readOnly, checkpointer: Boolean(checkpointer) && !readOnly, transaction,
363
+ /** Retries that recovered on this handle ({where, retries, at}); close() records them as one machine_logs warn row. */
364
+ recovered,
365
+ close() {
366
+ if (recovered.length) {
367
+ const row = { actor: 'harness', kind: 'machine-db.corrupt-recovered', level: 'warn', msg: `transient SQLITE_CORRUPT recovered ${recovered.length} time(s) on ${path.basename(file)}`,
368
+ data: { file, pid: process.pid, recovered: recovered.slice(0, 20), sqlite: process.versions.sqlite, node: process.version } };
369
+ recovered.length = 0;
370
+ // a reader cannot write: its notice waits in the outbox for the next flush
371
+ try {
372
+ if (readOnly) {
373
+ throw new Error('read-only handle');
374
+ }
375
+ API.log(m, row);
376
+ }
377
+ catch (error) {
378
+ try {
379
+ deferWrite({ op: 'log', args: [row], file, error });
380
+ }
381
+ catch { /* stderr already has it */ }
382
+ }
383
+ }
384
+ close();
385
+ } };
386
+ const m = core; // the typed functions are bound below, tempDirs after them
387
+ for (const [name, fn] of Object.entries(API))
388
+ m[name] = (...args) => {
389
+ if (!readOnly)
390
+ assertMutationFence({ kind: 'machine-access', name });
391
+ return fn(m, ...args);
392
+ };
393
+ m.tempDirs = tempDirs;
394
+ return m;
395
+ }
289
396
  // ---------------------------------------------------------------------------------------------------------------------
290
397
  // Blobs
291
398
  // ---------------------------------------------------------------------------------------------------------------------
292
399
  /** Store bytes (Buffer | string | file path via {file}) in the blob store and record the blobs row. Returns the sha. */
293
400
  function putMachineBlob(m, content, { mediaType = 'application/octet-stream', pinned = false } = {}) {
294
- // Every blob is redacted before it is stored (scripts/lib/redact.mjs): text media 'v1', anything else 'binary'.
295
- let raw = content; if (!Buffer.isBuffer(raw)) raw = Buffer.from(typeof raw === 'string' ? raw : JSON.stringify(raw));
296
- const { bytes, redaction } = redactBytes(raw, mediaType);
297
- const { sha, size, mediaType: stored } = storeBlob(bytes, { mediaType });
298
- const file = blobPath(sha);
299
- insertRow(m.db, 'blobs', { sha256: sha, bytes: size, media_type: stored, redaction, file_uri: String(file).replaceAll('\\', '/'), created_at: m.now(), pinned: pinned ? 1 : 0 }, { orIgnore: true });
300
- return sha;
401
+ // Every blob is redacted before it is stored (scripts/lib/redact.mjs): text media 'v1', anything else 'binary'.
402
+ const raw = Buffer.isBuffer(content) ? content : Buffer.from(typeof content === 'string' ? content : JSON.stringify(content));
403
+ const { bytes, redaction } = redactBytes(raw, mediaType);
404
+ const { sha, size, mediaType: stored } = storeBlob(bytes, { mediaType });
405
+ const file = blobPath(sha);
406
+ insertRow(m.db, 'blobs', { sha256: sha, bytes: size, media_type: stored, redaction, file_uri: String(file).replaceAll('\\', '/'), created_at: m.now(), pinned: pinned ? 1 : 0 }, { orIgnore: true });
407
+ return sha;
301
408
  }
302
409
  /** A JSON value as `{json, sha}`: inline when it fits `limit`, else a small stub inline and the full value as a blob. */
303
410
  function jsonOrBlob(m, value, limit = JSON_LIMIT) {
304
- if (value === undefined || value === null) return { json: null, sha: null };
305
- const text = typeof value === 'string' ? value : JSON.stringify(value);
306
- if (Buffer.byteLength(text) <= limit) return { json: text, sha: null };
307
- const sha = putMachineBlob(m, text, { mediaType: 'application/json' });
308
- return { json: JSON.stringify({ truncated: true, bytes: Buffer.byteLength(text), sha256: sha }), sha };
411
+ if (value === undefined || value === null)
412
+ return { json: null, sha: null };
413
+ const text = typeof value === 'string' ? value : JSON.stringify(value);
414
+ if (Buffer.byteLength(text) <= limit)
415
+ return { json: text, sha: null };
416
+ const sha = putMachineBlob(m, text, { mediaType: 'application/json' });
417
+ return { json: JSON.stringify({ truncated: true, bytes: Buffer.byteLength(text), sha256: sha }), sha };
309
418
  }
310
419
  /** The full JSON value behind a jsonOrBlob stub ({truncated, sha256}); the value itself otherwise. */
311
420
  export function fullJson(value) {
312
- if (value && typeof value === 'object' && value.truncated === true && typeof value.sha256 === 'string') {
313
- try { return JSON.parse(getBlob(value.sha256).toString('utf8')); } catch { return value; }
314
- }
315
- return value;
421
+ const stub = value;
422
+ if (stub && typeof stub === 'object' && stub.truncated === true && typeof stub.sha256 === 'string') {
423
+ try {
424
+ return JSON.parse(getBlob(stub.sha256).toString('utf8'));
425
+ }
426
+ catch {
427
+ return value;
428
+ }
429
+ }
430
+ return value;
316
431
  }
317
432
  const textBlob = (m, text, mediaType = 'text/plain') => (text == null || text === '' ? null : putMachineBlob(m, String(text), { mediaType }));
318
-
319
433
  // ---------------------------------------------------------------------------------------------------------------------
320
434
  // B0. Ledgers registry and repositories
321
435
  // ---------------------------------------------------------------------------------------------------------------------
322
- const ledgerRow = (row) => (row ? { ledgerId: row.ledger_id, name: row.name, product: row.product, repoRoot: row.repo_root, file: row.file, state: row.state,
323
- schemaVersion: row.schema_version, registeredAt: row.registered_at, seenAt: row.seen_at, retiredAt: row.retired_at, retiredReason: row.retired_reason } : null);
436
+ const ledgerOf = (row) => ({ ledgerId: row.ledger_id, name: row.name, product: row.product, repoRoot: row.repo_root, file: row.file, state: row.state,
437
+ schemaVersion: row.schema_version, registeredAt: row.registered_at, seenAt: row.seen_at, retiredAt: row.retired_at, retiredReason: row.retired_reason });
438
+ const ledgerRow = (row) => (row ? ledgerOf(row) : null);
324
439
  /** A ledger's own meta (key → value), read-only; {} when the file cannot be read. */
325
440
  function ledgerMetaOf(file) {
326
- let db = null;
327
- try {
328
- if (!fs.existsSync(file)) return {};
329
- const { DatabaseSync } = require('node:sqlite');
330
- db = new DatabaseSync(file, { readOnly: true, timeout: MACHINE_BUSY_TIMEOUT_MS });
331
- return Object.fromEntries(db.prepare('SELECT key, value FROM meta').all().map((r) => [r.key, r.value]));
332
- } catch { return {}; } finally { try { db?.close(); } catch { /* closed */ } }
441
+ let db = null;
442
+ try {
443
+ if (!fs.existsSync(file))
444
+ return {};
445
+ const { DatabaseSync } = require('node:sqlite');
446
+ db = new DatabaseSync(file, { readOnly: true, timeout: MACHINE_BUSY_TIMEOUT_MS });
447
+ return Object.fromEntries(db.prepare('SELECT key, value FROM meta').all().map((r) => [r.key, r.value]));
448
+ }
449
+ catch {
450
+ return {};
451
+ }
452
+ finally {
453
+ try {
454
+ db?.close();
455
+ }
456
+ catch { /* closed */ }
457
+ }
333
458
  }
334
459
  const repoKey = (root) => path.resolve(String(root)).replaceAll('\\', '/').replace(/^[a-z]:/, (d) => d.toUpperCase());
335
- const repoKeyOf = repoKey;
336
460
  // The live registry also refuses a repository under the OS temp directory (a repro or test run outside the isolated
337
461
  // registry would leave a junk workflow in the harness UI): typed refusal, nothing written.
338
462
  const tempRepoRefusal = (m, ledgerId, root) => (m.live && root && isUnderTempDir(String(root), { env: m.env, tempDirs: m.tempDirs })
339
- ? { ledgerId, registered: false, code: 'STARCI_REGISTRY_TEMP_REPO',
340
- refused: `registry-temp-repo: repo_root ${repoKey(root)} is under the OS temp directory and ${m.file} is the live registry; set ${TEST_REGISTRY_ENV}` } : null);
463
+ ? { ledgerId, registered: false, code: 'STARCI_REGISTRY_TEMP_REPO',
464
+ refused: `registry-temp-repo: repo_root ${repoKey(root)} is under the OS temp directory and ${m.file} is the live registry; set ${TEST_REGISTRY_ENV}` } : null);
341
465
  const ledgerRefresh = (existing, { target, at, name, repoRoot, product, schemaVersion }) => ({ file: target, seen_at: at, ...(name ? { name } : {}), ...(repoRoot ? { repo_root: repoKey(repoRoot) } : {}),
342
- ...(product ? { product } : {}), ...(schemaVersion != null ? { schema_version: int(schemaVersion) } : {}),
343
- ...(existing.state === 'retired' ? {} : { state: 'active' }) });
344
- /**
345
- * Register (or refresh) a ledger. `ledgerId` is the ledger's own meta.ledger_id. A new row needs name and repoRoot;
346
- * `file` defaults to projectLedgerFile(ledgerId). The live registry refuses a ledger file under the OS temp directory
347
- * (returns {registered:false, refused}); nothing is written then.
348
- */
466
+ ...(product ? { product } : {}), ...(schemaVersion != null ? { schema_version: int(schemaVersion) } : {}),
467
+ ...(existing.state === 'retired' ? {} : { state: 'active' }) });
349
468
  function registerLedger(m, { ledgerId, name = null, repoRoot = null, file = null, product = null, schemaVersion = null } = {}) {
350
- need(ledgerId, 'registerLedger needs the ledger meta.ledger_id');
351
- const target = path.resolve(file ?? projectLedgerFile(ledgerId, m.env));
352
- if (m.live && isUnderTempDir(target, { env: m.env, tempDirs: m.tempDirs }))
353
- return { ledgerId, registered: false, refused: `registry-temp-ledger: ${target} is under the OS temp directory and ${m.file} is the live registry; set ${TEST_REGISTRY_ENV}` };
354
- // A ledger opened by its writer names itself: repo_root / product come from its own meta when not given.
355
- const insertLedger = (db, at) => { if (!repoRoot) { const own = ledgerMetaOf(target); repoRoot = own.repo_root ?? null; product = product ?? own.product ?? null; }
356
- name = name ?? (repoRoot ? path.basename(path.resolve(repoRoot)) : null);
357
- if (!name || !repoRoot) return { ledgerId, registered: false, refused: `registry-no-repo-root: ${target} names no repo_root in its meta and none was given` };
358
- insertRow(db, 'ledgers', { ledger_id: ledgerId, name, product, repo_root: repoKey(repoRoot), file: target, state: 'active', schema_version: int(schemaVersion), registered_at: at, seen_at: at });
359
- return null; };
360
- return m.transaction((db) => {
361
- const at = m.now();
362
- const existing = db.prepare('SELECT * FROM ledgers WHERE ledger_id=?').get(ledgerId);
363
- const refusedRepo = tempRepoRefusal(m, ledgerId, repoRoot ?? (existing ? null : ledgerMetaOf(target).repo_root ?? null));
364
- if (refusedRepo) return refusedRepo;
365
- if (existing) updateRow(db, 'ledgers', ledgerRefresh(existing, { target, at, name, repoRoot, product, schemaVersion }), { ledger_id: ledgerId });
366
- else {
367
- const refusedInsert = insertLedger(db, at);
368
- if (refusedInsert) return refusedInsert;
369
- }
370
- if (repoRoot) upsertRow(db, 'repositories', { repo_root: repoKey(repoRoot), name: name ?? existing?.name ?? path.basename(repoRoot), role: 'backend', ledger_id: ledgerId, seen_at: at }, ['repo_root']);
371
- return { ledgerId, registered: true, file: target };
372
- });
469
+ need(ledgerId, 'registerLedger needs the ledger meta.ledger_id');
470
+ const target = path.resolve(file ?? projectLedgerFile(ledgerId, m.env));
471
+ if (m.live && isUnderTempDir(target, { env: m.env, tempDirs: m.tempDirs }))
472
+ return { ledgerId, registered: false, refused: `registry-temp-ledger: ${target} is under the OS temp directory and ${m.file} is the live registry; set ${TEST_REGISTRY_ENV}` };
473
+ // A ledger opened by its writer names itself: repo_root / product come from its own meta when not given.
474
+ const insertLedger = (db, at) => {
475
+ if (!repoRoot) {
476
+ const own = ledgerMetaOf(target);
477
+ repoRoot = own.repo_root ?? null;
478
+ product = product ?? own.product ?? null;
479
+ }
480
+ name = name ?? (repoRoot ? path.basename(path.resolve(repoRoot)) : null);
481
+ if (!name || !repoRoot)
482
+ return { ledgerId, registered: false, refused: `registry-no-repo-root: ${target} names no repo_root in its meta and none was given` };
483
+ insertRow(db, 'ledgers', { ledger_id: ledgerId, name, product, repo_root: repoKey(repoRoot), file: target, state: 'active', schema_version: int(schemaVersion), registered_at: at, seen_at: at });
484
+ return null;
485
+ };
486
+ return m.transaction((db) => {
487
+ const at = m.now();
488
+ const existing = db.prepare('SELECT * FROM ledgers WHERE ledger_id=?').get(ledgerId);
489
+ const refusedRepo = tempRepoRefusal(m, ledgerId, repoRoot ?? (existing ? null : ledgerMetaOf(target).repo_root ?? null));
490
+ if (refusedRepo)
491
+ return refusedRepo;
492
+ if (existing)
493
+ updateRow(db, 'ledgers', ledgerRefresh(existing, { target, at, name, repoRoot, product, schemaVersion }), { ledger_id: ledgerId });
494
+ else {
495
+ const refusedInsert = insertLedger(db, at);
496
+ if (refusedInsert)
497
+ return refusedInsert;
498
+ }
499
+ if (repoRoot)
500
+ upsertRow(db, 'repositories', { repo_root: repoKey(repoRoot), name: name ?? existing?.name ?? path.basename(repoRoot), role: 'backend', ledger_id: ledgerId, seen_at: at }, ['repo_root']);
501
+ return { ledgerId, registered: true, file: target };
502
+ });
373
503
  }
374
504
  /**
375
505
  * The ledger for a repository root, name or id; null when unregistered. `create:true` with repoRoot (and name) mints a
@@ -377,299 +507,349 @@ function registerLedger(m, { ledgerId, name = null, repoRoot = null, file = null
377
507
  * with meta.ledger_id = ledgerId.
378
508
  */
379
509
  function resolveLedger(m, { ledgerId = null, name = null, repoRoot = null, create = false, product = null } = {}) {
380
- const db = m.db;
381
- let row = null;
382
- if (ledgerId) row = db.prepare('SELECT * FROM ledgers WHERE ledger_id=?').get(ledgerId);
383
- else if (repoRoot) row = db.prepare("SELECT * FROM ledgers WHERE repo_root=? ORDER BY state='retired', seen_at DESC LIMIT 1").get(repoKey(repoRoot));
384
- else if (name) row = db.prepare('SELECT * FROM ledgers WHERE name=?').get(name);
385
- if (row || !create) return ledgerRow(row);
386
- need(repoRoot, 'resolveLedger create needs repoRoot');
387
- const id = crypto.randomUUID();
388
- const made = registerLedger(m, { ledgerId: id, name: name ?? path.basename(path.resolve(repoRoot)), repoRoot, product });
389
- if (made.refused) throw Object.assign(new Error(made.refused), { code: made.code ?? 'STARCI_MACHINE_DB' });
390
- return ledgerRow(db.prepare('SELECT * FROM ledgers WHERE ledger_id=?').get(id));
510
+ const db = m.db;
511
+ let row = null;
512
+ if (ledgerId)
513
+ row = db.prepare('SELECT * FROM ledgers WHERE ledger_id=?').get(ledgerId);
514
+ else if (repoRoot)
515
+ row = db.prepare("SELECT * FROM ledgers WHERE repo_root=? ORDER BY state='retired', seen_at DESC LIMIT 1").get(repoKey(repoRoot));
516
+ else if (name)
517
+ row = db.prepare('SELECT * FROM ledgers WHERE name=?').get(name);
518
+ if (row || !create)
519
+ return ledgerRow(row);
520
+ need(repoRoot, 'resolveLedger create needs repoRoot');
521
+ const id = crypto.randomUUID();
522
+ const made = registerLedger(m, { ledgerId: id, name: name ?? path.basename(path.resolve(repoRoot)), repoRoot, product });
523
+ if (made.refused)
524
+ throw Object.assign(new Error(made.refused), { code: made.code ?? 'STARCI_MACHINE_DB' });
525
+ return ledgerRow(db.prepare('SELECT * FROM ledgers WHERE ledger_id=?').get(id));
391
526
  }
392
527
  function listLedgers(m, { state = null, includeRetired = false } = {}) {
393
- let rows; if (state) rows = m.db.prepare('SELECT * FROM ledgers WHERE state=? ORDER BY name').all(state); else rows = m.db.prepare(`SELECT * FROM ledgers ${includeRetired ? '' : "WHERE state<>'retired'"} ORDER BY name`).all();
394
- return rows.map(ledgerRow);
528
+ let rows;
529
+ if (state)
530
+ rows = m.db.prepare('SELECT * FROM ledgers WHERE state=? ORDER BY name').all(state);
531
+ else
532
+ rows = m.db.prepare(`SELECT * FROM ledgers ${includeRetired ? '' : "WHERE state<>'retired'"} ORDER BY name`).all();
533
+ return rows.map(ledgerOf);
395
534
  }
396
535
  function touchLedger(m, ledgerId, { at = m.now() } = {}) { return m.db.prepare('UPDATE ledgers SET seen_at=? WHERE ledger_id=?').run(at, ledgerId).changes > 0; }
397
536
  function setLedgerState(m, ledgerId, state, { reason = null } = {}) {
398
- const at = m.now();
399
- return m.db.prepare('UPDATE ledgers SET state=?, retired_at=CASE WHEN ?=\'retired\' THEN ? ELSE NULL END, retired_reason=CASE WHEN ?=\'retired\' THEN ? ELSE NULL END WHERE ledger_id=?')
400
- .run(state, state, at, state, reason, ledgerId).changes > 0;
537
+ const at = m.now();
538
+ return m.db.prepare('UPDATE ledgers SET state=?, retired_at=CASE WHEN ?=\'retired\' THEN ? ELSE NULL END, retired_reason=CASE WHEN ?=\'retired\' THEN ? ELSE NULL END WHERE ledger_id=?')
539
+ .run(state, state, at, state, reason, ledgerId).changes > 0;
401
540
  }
402
541
  function upsertRepository(m, { repoRoot, name, role, ledgerId = null, defaultBranch = null, remote = null }) {
403
- return upsertRow(m.db, 'repositories', { repo_root: repoKey(repoRoot), name, role, ledger_id: ledgerId, default_branch: defaultBranch, remote, seen_at: m.now() }, ['repo_root']);
542
+ return upsertRow(m.db, 'repositories', { repo_root: repoKey(repoRoot), name, role, ledger_id: ledgerId, default_branch: defaultBranch, remote, seen_at: m.now() }, ['repo_root']);
404
543
  }
405
544
  /**
406
545
  * Run fn({ledger, db}) over every registered ledger opened READ-ONLY, one at a time (B8: no ATTACH-UNION). A ledger that
407
546
  * cannot be opened yields {ledger, error}. Returns the array of results.
408
547
  */
409
548
  function forEachLedger(m, fn, { state = 'active' } = {}) {
410
- const out = [];
411
- for (const ledger of listLedgers(m, { state })) {
412
- let db = null;
413
- try {
414
- need(fs.existsSync(ledger.file), `ledger file missing: ${ledger.file}`);
415
- db = openReadOnlyDb(ledger.file, MACHINE_BUSY_TIMEOUT_MS);
416
- db.exec('PRAGMA query_only=ON;');
417
- out.push({ ledger, result: fn({ ledger, db }) });
418
- } catch (error) { out.push({ ledger, error: String(error?.message ?? error) }); } finally { try { db?.close(); } catch { /* closed */ } }
419
- }
420
- return out;
549
+ const out = [];
550
+ for (const ledger of listLedgers(m, { state })) {
551
+ let db = null;
552
+ try {
553
+ need(fs.existsSync(ledger.file), `ledger file missing: ${ledger.file}`);
554
+ db = openReadOnlyDb(ledger.file, MACHINE_BUSY_TIMEOUT_MS);
555
+ db.exec('PRAGMA query_only=ON;');
556
+ out.push({ ledger, result: fn({ ledger, db }) });
557
+ }
558
+ catch (error) {
559
+ out.push({ ledger, error: String(error?.message ?? error) });
560
+ }
561
+ finally {
562
+ try {
563
+ db?.close();
564
+ }
565
+ catch { /* closed */ }
566
+ }
567
+ }
568
+ return out;
421
569
  }
422
570
  /** Manual queries only: ATTACH up to 9 ledgers read-only to this handle as l0..l8. Returns the attached names. */
423
571
  function attachLedgers(m, ledgers = listLedgers(m)) {
424
- need(ledgers.length <= 9, 'attachLedgers: at most 9 ledgers per batch');
425
- return ledgers.map((l, i) => { m.db.exec(`ATTACH DATABASE ${JSON.stringify(pathToFileURL(l.file).href + '?mode=ro')} AS l${i}`); return { alias: `l${i}`, ...l }; });
572
+ need(ledgers.length <= 9, 'attachLedgers: at most 9 ledgers per batch');
573
+ return ledgers.map((l, i) => { m.db.exec(`ATTACH DATABASE ${JSON.stringify(pathToFileURL(l.file).href + '?mode=ro')} AS l${i}`); return { alias: `l${i}`, ...l }; });
426
574
  }
427
-
428
575
  // ---------------------------------------------------------------------------------------------------------------------
429
576
  // B1. Supervisor
430
577
  // ---------------------------------------------------------------------------------------------------------------------
431
578
  /** Append one sup_events row; the digest chain is computed here (JS), inside the caller's or a new transaction. */
432
579
  function supEvent(m, { entityType = 'supervisor', entityId = 'main', kind, payload = null, spanId = null, at = m.now(), eventId = null }) {
433
- need(kind, 'supEvent needs kind');
434
- return m.transaction((db) => {
435
- const { json, sha } = jsonOrBlob(m, payload == null ? null : redactData(payload), EVENT_LIMITS.payloadBytes);
436
- const prev = db.prepare('SELECT digest FROM sup_events ORDER BY seq DESC LIMIT 1').get()?.digest ?? null;
437
- const id = eventId ?? crypto.randomUUID();
438
- const digest = sha256([prev ?? '', id, entityType, entityId, kind, json ?? '', sha ?? '', at].join('\n'));
439
- const r = insertRow(db, 'sup_events', { event_id: id, entity_type: entityType, entity_id: String(entityId), kind, span_id: spanId, payload_json: json,
440
- payload_sha: sha, prev_digest: prev, digest, created_at: at });
441
- return { seq: Number(r.lastInsertRowid), eventId: id, digest };
442
- });
580
+ need(kind, 'supEvent needs kind');
581
+ return m.transaction((db) => {
582
+ const { json, sha } = jsonOrBlob(m, payload == null ? null : redactData(payload), EVENT_LIMITS.payloadBytes);
583
+ const prev = db.prepare('SELECT digest FROM sup_events ORDER BY seq DESC LIMIT 1').get()?.digest ?? null;
584
+ const id = eventId ?? crypto.randomUUID();
585
+ const digest = sha256([prev ?? '', id, entityType, entityId, kind, json ?? '', sha ?? '', at].join('\n'));
586
+ const r = insertRow(db, 'sup_events', { event_id: id, entity_type: entityType, entity_id: String(entityId), kind, span_id: spanId, payload_json: json,
587
+ payload_sha: sha, prev_digest: prev, digest, created_at: at });
588
+ return { seq: Number(r.lastInsertRowid), eventId: id, digest };
589
+ });
443
590
  }
444
591
  function supEvents(m, { kind = null, kinds = null, entityType = null, entityId = null, since = null, limit = 200, order = 'desc' } = {}) {
445
- const where = [], args = [];
446
- const list = kinds ?? (kind ? [kind] : null);
447
- if (list) { where.push(`kind IN (${list.map(() => '?').join(',')})`); args.push(...list); }
448
- if (entityType) { where.push('entity_type=?'); args.push(entityType); }
449
- if (entityId) { where.push('entity_id=?'); args.push(String(entityId)); }
450
- if (since != null) { where.push('seq>?'); args.push(since); }
451
- const rows = m.db.prepare(`SELECT * FROM sup_events ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY seq ${order === 'asc' ? 'ASC' : 'DESC'} LIMIT ?`).all(...args, limit);
452
- return rows.map((r) => ({ ...r, payload: fullJson(parse(r.payload_json)) }));
592
+ const where = [], args = [];
593
+ const list = kinds ?? (kind ? [kind] : null);
594
+ if (list) {
595
+ where.push(`kind IN (${list.map(() => '?').join(',')})`);
596
+ args.push(...list);
597
+ }
598
+ if (entityType) {
599
+ where.push('entity_type=?');
600
+ args.push(entityType);
601
+ }
602
+ if (entityId) {
603
+ where.push('entity_id=?');
604
+ args.push(String(entityId));
605
+ }
606
+ if (since != null) {
607
+ where.push('seq>?');
608
+ args.push(since);
609
+ }
610
+ const rows = m.db.prepare(`SELECT * FROM sup_events ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY seq ${order === 'asc' ? 'ASC' : 'DESC'} LIMIT ?`).all(...args, limit);
611
+ return rows.map((r) => ({ ...r, payload: fullJson(parse(r.payload_json)) }));
453
612
  }
454
613
  const newestSupEvent = (m, kind) => supEvents(m, { kind, limit: 1 })[0] ?? null;
455
-
456
614
  function upsertSupJob(m, { jobId, traceId = null, kind, role = 'worker', cluster = null, title, status = 'queued', lane = null, files = null, brief = null, payload = null }) {
457
- const at = m.now();
458
- const existing = m.db.prepare('SELECT trace_id, created_at FROM sup_jobs WHERE job_id=?').get(jobId);
459
- return upsertRow(m.db, 'sup_jobs', { job_id: jobId, trace_id: existing?.trace_id ?? traceId ?? newTraceId(), kind, role, cluster, title, status, lane,
460
- files_json: files, brief, payload_json: payload, created_at: existing?.created_at ?? at, updated_at: at }, ['job_id']);
615
+ const at = m.now();
616
+ const existing = m.db.prepare('SELECT trace_id, created_at FROM sup_jobs WHERE job_id=?').get(jobId);
617
+ return upsertRow(m.db, 'sup_jobs', { job_id: jobId, trace_id: existing?.trace_id ?? traceId ?? newTraceId(), kind, role, cluster, title, status, lane,
618
+ files_json: files, brief, payload_json: payload, created_at: existing?.created_at ?? at, updated_at: at }, ['job_id']);
461
619
  }
462
620
  function setSupJobStatus(m, jobId, status, { payload = undefined } = {}) {
463
- return m.transaction(() => {
464
- const changed = updateRow(m.db, 'sup_jobs', { status, updated_at: m.now(), ...(payload !== undefined ? { payload_json: payload } : {}) }, { job_id: jobId }).changes;
465
- if (changed) supEvent(m, { entityType: 'sup-job', entityId: jobId, kind: `sup-job-${status}` });
466
- return changed > 0;
467
- });
621
+ return m.transaction(() => {
622
+ const changed = updateRow(m.db, 'sup_jobs', { status, updated_at: m.now(), ...(payload !== undefined ? { payload_json: payload } : {}) }, { job_id: jobId }).changes;
623
+ if (changed)
624
+ supEvent(m, { entityType: 'sup-job', entityId: jobId, kind: `sup-job-${status}` });
625
+ return changed > 0;
626
+ });
468
627
  }
469
628
  const supJob = (m, jobId) => { const r = m.db.prepare('SELECT * FROM sup_jobs WHERE job_id=?').get(jobId); return r ? { ...r, files: parse(r.files_json), payload: parse(r.payload_json) } : null; };
470
629
  function listSupJobs(m, { status = null, statuses = null, kind = null } = {}) {
471
- const where = [], args = [];
472
- const list = statuses ?? (status ? [status] : null);
473
- if (list) { where.push(`status IN (${list.map(() => '?').join(',')})`); args.push(...list); }
474
- if (kind) { where.push('kind=?'); args.push(kind); }
475
- return m.db.prepare(`SELECT * FROM sup_jobs ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY created_at, job_id`).all(...args)
476
- .map((r) => ({ ...r, files: parse(r.files_json), payload: parse(r.payload_json) }));
630
+ const where = [], args = [];
631
+ const list = statuses ?? (status ? [status] : null);
632
+ if (list) {
633
+ where.push(`status IN (${list.map(() => '?').join(',')})`);
634
+ args.push(...list);
635
+ }
636
+ if (kind) {
637
+ where.push('kind=?');
638
+ args.push(kind);
639
+ }
640
+ return m.db.prepare(`SELECT * FROM sup_jobs ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY created_at, job_id`).all(...args)
641
+ .map((r) => ({ ...r, files: parse(r.files_json), payload: parse(r.payload_json) }));
477
642
  }
478
643
  function acquireSupLeases(m, jobId, paths, { ttlMs = 3600000 } = {}) {
479
- return m.transaction((db) => {
480
- const at = m.now();
481
- db.prepare('DELETE FROM sup_leases WHERE expires_at<?').run(at);
482
- const held = paths.map((p) => db.prepare('SELECT job_id FROM sup_leases WHERE path=?').get(p)).map((r, i) => (r && r.job_id !== jobId ? { path: paths[i], holder: r.job_id } : null)).filter(Boolean);
483
- if (held.length) return { ok: false, conflicts: held };
484
- for (const p of paths) upsertRow(db, 'sup_leases', { path: p, job_id: jobId, acquired_at: at, expires_at: at + ttlMs }, ['path']);
485
- return { ok: true };
486
- });
644
+ return m.transaction((db) => {
645
+ const at = m.now();
646
+ db.prepare('DELETE FROM sup_leases WHERE expires_at<?').run(at);
647
+ const held = paths.map((p) => db.prepare('SELECT job_id FROM sup_leases WHERE path=?').get(p)).map((r, i) => (r && r.job_id !== jobId ? { path: paths[i], holder: r.job_id } : null)).filter(Boolean);
648
+ if (held.length)
649
+ return { ok: false, conflicts: held };
650
+ for (const p of paths)
651
+ upsertRow(db, 'sup_leases', { path: p, job_id: jobId, acquired_at: at, expires_at: at + ttlMs }, ['path']);
652
+ return { ok: true };
653
+ });
487
654
  }
488
655
  const releaseSupLeases = (m, jobId) => m.db.prepare('DELETE FROM sup_leases WHERE job_id=?').run(jobId).changes;
489
656
  const supLeases = (m) => m.db.prepare('SELECT * FROM sup_leases ORDER BY path').all();
490
657
  function startSupAttempt(m, { jobId, spanId = newSpanId(), parentSpanId = null, ...rest }) {
491
- const seq = Number(m.db.prepare('SELECT COALESCE(max(dispatch_seq),0)+1 n FROM sup_attempts WHERE job_id=?').get(jobId).n);
492
- const cols = snake(rest);
493
- const r = insertRow(m.db, 'sup_attempts', { job_id: jobId, dispatch_seq: seq, span_id: spanId, parent_span_id: parentSpanId, spawned_at: m.now(), ...cols });
494
- return { attemptId: Number(r.lastInsertRowid), dispatchSeq: seq, spanId };
658
+ const seq = Number(m.db.prepare('SELECT COALESCE(max(dispatch_seq),0)+1 n FROM sup_attempts WHERE job_id=?').get(jobId)?.n);
659
+ const cols = snake(rest);
660
+ const r = insertRow(m.db, 'sup_attempts', { job_id: jobId, dispatch_seq: seq, span_id: spanId, parent_span_id: parentSpanId, spawned_at: m.now(), ...cols });
661
+ return { attemptId: Number(r.lastInsertRowid), dispatchSeq: seq, spanId };
495
662
  }
496
663
  const updateSupAttempt = (m, attemptId, fields) => updateRow(m.db, 'sup_attempts', snake(fields), { attempt_id: attemptId }).changes > 0;
497
664
  const latestSupAttempt = (m, jobId) => m.db.prepare('SELECT * FROM sup_attempts WHERE job_id=? ORDER BY dispatch_seq DESC LIMIT 1').get(jobId) ?? null;
498
665
  function recordSupReport(m, { attemptId, jobId, outcome, report, reportText = null }) {
499
- const reportSha = reportText ? textBlob(m, reportText, 'text/markdown') : null;
500
- const r = upsertRow(m.db, 'sup_reports', { attempt_id: attemptId, job_id: jobId, outcome, report_json: report ?? {}, report_sha: reportSha, created_at: m.now() }, ['attempt_id']);
501
- return { reportId: Number(r.lastInsertRowid), reportSha };
666
+ const reportSha = reportText ? textBlob(m, reportText, 'text/markdown') : null;
667
+ const r = upsertRow(m.db, 'sup_reports', { attempt_id: attemptId, job_id: jobId, outcome, report_json: report ?? {}, report_sha: reportSha, created_at: m.now() }, ['attempt_id']);
668
+ return { reportId: Number(r.lastInsertRowid), reportSha };
502
669
  }
503
670
  const supReports = (m, { jobId = null, unconsumed = false } = {}) => m.db.prepare(`SELECT * FROM sup_reports WHERE 1=1 ${jobId ? 'AND job_id=?' : ''} ${unconsumed ? 'AND consumed_at IS NULL' : ''} ORDER BY report_id`)
504
- .all(...(jobId ? [jobId] : [])).map((r) => ({ ...r, report: parse(r.report_json) }));
671
+ .all(...(jobId ? [jobId] : [])).map((r) => ({ ...r, report: parse(r.report_json) }));
505
672
  const consumeSupReport = (m, reportId) => m.db.prepare('UPDATE sup_reports SET consumed_at=? WHERE report_id=? AND consumed_at IS NULL').run(m.now(), reportId).changes > 0;
506
-
507
673
  /**
508
674
  * Open a Supervisor Decision Item, idempotent on its key (MB-07: '<kind>:<entity>:<signature>:<head>', no empty part).
509
675
  * Returns {diId, created}. An existing open item with the same key is returned unchanged.
510
676
  */
511
- function openSupDecision(m, { keyParts, kind, decider = 'supervisor', summary, ledgerId = null, workflowId = null, entityType = null, entityId = null,
512
- openedBy = 'reconciler', dueAt = null, escalateTo = null, evidence = null, options = null, allowedVerbs = null, payload = {} }) {
513
- need(keyParts && typeof keyParts === 'object' && !Array.isArray(keyParts), 'openSupDecision needs keyParts {kind, entity, signature, head, ...}');
514
- const parts = Object.values(keyParts).map((v) => String(v ?? ''));
515
- need(parts.length >= 2 && parts.every((p) => p.trim() && !p.includes(':')), `openSupDecision: empty or ':'-bearing key part in ${JSON.stringify(keyParts)} (MB-07)`);
516
- const key = parts.join(':');
517
- return m.transaction((db) => {
518
- const hit = db.prepare('SELECT di_id, status FROM sup_decision_items WHERE idempotency_key=?').get(key);
519
- if (hit) return { diId: hit.di_id, created: false, status: hit.status };
520
- const diId = `sdi-${crypto.randomUUID()}`;
521
- insertRow(db, 'sup_decision_items', { di_id: diId, idempotency_key: key, key_parts_json: keyParts, ledger_id: ledgerId, workflow_id: workflowId, kind, decider,
522
- entity_type: entityType, entity_id: entityId, summary, status: 'open', opened_by: openedBy, opened_at: m.now(), due_at: dueAt, escalate_to: escalateTo,
523
- evidence_json: evidence, options_json: options, allowed_verbs_json: allowedVerbs, payload_json: payload ?? {} });
524
- supEvent(m, { entityType: 'sup-decision', entityId: diId, kind: 'sup-decision-opened', payload: { key, kind, summary } });
525
- return { diId, created: true, status: 'open' };
526
- });
677
+ function openSupDecision(m, { keyParts, kind, decider = 'supervisor', summary, ledgerId = null, workflowId = null, entityType = null, entityId = null, openedBy = 'reconciler', dueAt = null, escalateTo = null, evidence = null, options = null, allowedVerbs = null, payload = {} }) {
678
+ need(keyParts && typeof keyParts === 'object' && !Array.isArray(keyParts), 'openSupDecision needs keyParts {kind, entity, signature, head, ...}');
679
+ const parts = Object.values(keyParts).map((v) => String(v ?? ''));
680
+ need(parts.length >= 2 && parts.every((p) => p.trim() && !p.includes(':')), `openSupDecision: empty or ':'-bearing key part in ${JSON.stringify(keyParts)} (MB-07)`);
681
+ const key = parts.join(':');
682
+ return m.transaction((db) => {
683
+ const hit = db.prepare('SELECT di_id, status FROM sup_decision_items WHERE idempotency_key=?').get(key);
684
+ if (hit)
685
+ return { diId: hit.di_id, created: false, status: hit.status };
686
+ const diId = `sdi-${crypto.randomUUID()}`;
687
+ insertRow(db, 'sup_decision_items', { di_id: diId, idempotency_key: key, key_parts_json: keyParts, ledger_id: ledgerId, workflow_id: workflowId, kind, decider,
688
+ entity_type: entityType, entity_id: entityId, summary, status: 'open', opened_by: openedBy, opened_at: m.now(), due_at: dueAt, escalate_to: escalateTo,
689
+ evidence_json: evidence, options_json: options, allowed_verbs_json: allowedVerbs, payload_json: payload ?? {} });
690
+ supEvent(m, { entityType: 'sup-decision', entityId: diId, kind: 'sup-decision-opened', payload: { key, kind, summary } });
691
+ return { diId, created: true, status: 'open' };
692
+ });
527
693
  }
528
694
  function setSupDecision(m, diId, { status, by = null, verb = null, choice = null, rationale = null, result = null, supersededBy = null, spanId = newSpanId() }) {
529
- return m.transaction((db) => {
530
- const at = m.now();
531
- const set = { status };
532
- if (status === 'claimed') Object.assign(set, { claim_by: by, claim_at: at });
533
- if (status === 'escalated') db.prepare('UPDATE sup_decision_items SET escalations=escalations+1 WHERE di_id=?').run(diId);
534
- if (status === 'superseded') set.superseded_by = supersededBy;
535
- let decisionId = null;
536
- if (status === 'resolved') {
537
- decisionId = `sdec-${crypto.randomUUID()}`;
538
- const di = db.prepare('SELECT * FROM sup_decision_items WHERE di_id=?').get(diId);
539
- insertRow(db, 'sup_decisions', { decision_id: decisionId, di_id: diId, decider: by ?? di?.decider ?? 'supervisor', span_id: spanId, ledger_id: di?.ledger_id, workflow_id: di?.workflow_id,
540
- subject_type: di?.entity_type, subject_id: di?.entity_id, choice: choice ?? verb ?? 'resolved', rationale, result_json: result, decided_at: at });
541
- Object.assign(set, { resolved_by: by, resolved_at: at, resolution_verb: verb, decision_id: decisionId });
542
- }
543
- const changed = updateRow(db, 'sup_decision_items', set, { di_id: diId }).changes;
544
- if (changed) supEvent(m, { entityType: 'sup-decision', entityId: diId, kind: `sup-decision-${status}`, payload: { by, verb } });
545
- return { changed: changed > 0, decisionId };
546
- });
695
+ return m.transaction((db) => {
696
+ const at = m.now();
697
+ const set = { status };
698
+ if (status === 'claimed')
699
+ Object.assign(set, { claim_by: by, claim_at: at });
700
+ if (status === 'escalated')
701
+ db.prepare('UPDATE sup_decision_items SET escalations=escalations+1 WHERE di_id=?').run(diId);
702
+ if (status === 'superseded')
703
+ set.superseded_by = supersededBy;
704
+ let decisionId = null;
705
+ if (status === 'resolved') {
706
+ decisionId = `sdec-${crypto.randomUUID()}`;
707
+ const di = db.prepare('SELECT * FROM sup_decision_items WHERE di_id=?').get(diId);
708
+ insertRow(db, 'sup_decisions', { decision_id: decisionId, di_id: diId, decider: by ?? di?.decider ?? 'supervisor', span_id: spanId, ledger_id: di?.ledger_id, workflow_id: di?.workflow_id,
709
+ subject_type: di?.entity_type, subject_id: di?.entity_id, choice: choice ?? verb ?? 'resolved', rationale, result_json: result, decided_at: at });
710
+ Object.assign(set, { resolved_by: by, resolved_at: at, resolution_verb: verb, decision_id: decisionId });
711
+ }
712
+ const changed = updateRow(db, 'sup_decision_items', set, { di_id: diId }).changes;
713
+ if (changed)
714
+ supEvent(m, { entityType: 'sup-decision', entityId: diId, kind: `sup-decision-${status}`, payload: { by, verb } });
715
+ return { changed: changed > 0, decisionId };
716
+ });
547
717
  }
548
718
  const markSupDecisionDelivered = (m, diId) => m.db.prepare('UPDATE sup_decision_items SET delivered_at=COALESCE(delivered_at,?) WHERE di_id=?').run(m.now(), diId).changes > 0;
549
719
  function listSupDecisions(m, { open = true, kind = null } = {}) {
550
- return m.db.prepare(`SELECT * FROM sup_decision_items WHERE 1=1 ${open ? "AND status IN ('open','claimed','escalated')" : ''} ${kind ? 'AND kind=?' : ''} ORDER BY opened_at`)
551
- .all(...(kind ? [kind] : [])).map((r) => ({ ...r, keyParts: parse(r.key_parts_json), payload: parse(r.payload_json), evidence: parse(r.evidence_json) }));
720
+ return m.db.prepare(`SELECT * FROM sup_decision_items WHERE 1=1 ${open ? "AND status IN ('open','claimed','escalated')" : ''} ${kind ? 'AND kind=?' : ''} ORDER BY opened_at`)
721
+ .all(...(kind ? [kind] : [])).map((r) => ({ ...r, keyParts: parse(r.key_parts_json), payload: parse(r.payload_json), evidence: parse(r.evidence_json) }));
552
722
  }
553
723
  function openOwed(m, { owedId, kind, subject, cluster = null, dueAt = null, detail = null }) {
554
- return m.transaction((db) => {
555
- const hit = db.prepare('SELECT state FROM sup_owed WHERE owed_id=?').get(owedId);
556
- if (hit) { if (detail !== null) { updateRow(db, 'sup_owed', { detail_json: detail, ...(dueAt ? { due_at: dueAt } : {}) }, { owed_id: owedId }); } return { owedId, created: false, state: hit.state }; }
557
- insertRow(db, 'sup_owed', { owed_id: owedId, kind, subject, cluster, state: 'open', opened_at: m.now(), due_at: dueAt, detail_json: detail });
558
- return { owedId, created: true, state: 'open' };
559
- });
724
+ return m.transaction((db) => {
725
+ const hit = db.prepare('SELECT state FROM sup_owed WHERE owed_id=?').get(owedId);
726
+ if (hit) {
727
+ if (detail !== null) {
728
+ updateRow(db, 'sup_owed', { detail_json: detail, ...(dueAt ? { due_at: dueAt } : {}) }, { owed_id: owedId });
729
+ }
730
+ return { owedId, created: false, state: hit.state };
731
+ }
732
+ insertRow(db, 'sup_owed', { owed_id: owedId, kind, subject, cluster, state: 'open', opened_at: m.now(), due_at: dueAt, detail_json: detail });
733
+ return { owedId, created: true, state: 'open' };
734
+ });
560
735
  }
561
736
  const ackOwed = (m, owedId, { by = 'supervisor' } = {}) => m.db.prepare("UPDATE sup_owed SET state='acked', acked_at=?, acked_by=? WHERE owed_id=? AND state='open'").run(m.now(), by, owedId).changes > 0;
562
737
  const closeOwed = (m, owedId) => m.db.prepare("UPDATE sup_owed SET state='closed', closed_at=? WHERE owed_id=? AND state<>'closed'").run(m.now(), owedId).changes > 0;
563
738
  const listOwed = (m, { open = true } = {}) => m.db.prepare(`SELECT * FROM sup_owed ${open ? "WHERE state<>'closed'" : ''} ORDER BY opened_at`).all().map((r) => ({ ...r, detail: parse(r.detail_json) }));
564
739
  function upsertLearning(m, { itemId, kind, parentId = null, title, state = null, sourceRef = null, lane = null, landedSha = null, detail = null }) {
565
- const at = m.now();
566
- const created = m.db.prepare('SELECT created_at FROM sup_learning WHERE item_id=?').get(itemId)?.created_at ?? at;
567
- return upsertRow(m.db, 'sup_learning', { item_id: itemId, kind, parent_id: parentId, title, state, source_ref: sourceRef, lane, landed_sha: landedSha, detail_json: detail, created_at: created, updated_at: at }, ['item_id']);
740
+ const at = m.now();
741
+ const created = m.db.prepare('SELECT created_at FROM sup_learning WHERE item_id=?').get(itemId)?.created_at ?? at;
742
+ return upsertRow(m.db, 'sup_learning', { item_id: itemId, kind, parent_id: parentId, title, state, source_ref: sourceRef, lane, landed_sha: landedSha, detail_json: detail, created_at: created, updated_at: at }, ['item_id']);
568
743
  }
569
744
  const listLearning = (m, { kind = null } = {}) => m.db.prepare(`SELECT * FROM sup_learning ${kind ? 'WHERE kind=?' : ''} ORDER BY created_at, item_id`).all(...(kind ? [kind] : [])).map((r) => ({ ...r, detail: parse(r.detail_json) }));
570
745
  const recordOwnerRuling = (m, { rulingId = `rul-${crypto.randomUUID()}`, saidAt, channel = null, verbatim, paraphrase = null, appliesTo = null, contractRef = null, recordedBy = null }) => {
571
- insertRow(m.db, 'sup_owner_rulings', { ruling_id: rulingId, said_at: saidAt, channel, verbatim, paraphrase, applies_to: appliesTo, contract_ref: contractRef, recorded_by: recordedBy, created_at: m.now() }); return rulingId; };
746
+ insertRow(m.db, 'sup_owner_rulings', { ruling_id: rulingId, said_at: saidAt, channel, verbatim, paraphrase, applies_to: appliesTo, contract_ref: contractRef, recorded_by: recordedBy, created_at: m.now() });
747
+ return rulingId;
748
+ };
572
749
  function upsertBridge(m, { bridgeId, ledgerId = null, action, state, approvedBy = null, detail = null }) {
573
- const at = m.now();
574
- const created = m.db.prepare('SELECT created_at FROM sup_bridges WHERE bridge_id=?').get(bridgeId)?.created_at ?? at;
575
- return upsertRow(m.db, 'sup_bridges', { bridge_id: bridgeId, ledger_id: ledgerId, action, state, approved_by: approvedBy, detail_json: detail, created_at: created, updated_at: at }, ['bridge_id']);
750
+ const at = m.now();
751
+ const created = m.db.prepare('SELECT created_at FROM sup_bridges WHERE bridge_id=?').get(bridgeId)?.created_at ?? at;
752
+ return upsertRow(m.db, 'sup_bridges', { bridge_id: bridgeId, ledger_id: ledgerId, action, state, approved_by: approvedBy, detail_json: detail, created_at: created, updated_at: at }, ['bridge_id']);
576
753
  }
577
754
  function recordSupMessage(m, { msgId = `msg-${crypto.randomUUID()}`, direction, channel, chatId = null, messageId = null, from = null, to = null, via = null, text, ok = null, at = m.now() }) {
578
- insertRow(m.db, 'sup_messages', { msg_id: msgId, direction, channel, chat_id: chatId, message_id: messageId, from_ref: from, to_ref: to, via, text, ok: bool(ok), at }, { orIgnore: true });
579
- return msgId;
755
+ insertRow(m.db, 'sup_messages', { msg_id: msgId, direction, channel, chat_id: chatId, message_id: messageId, from_ref: from, to_ref: to, via, text, ok: bool(ok), at }, { orIgnore: true });
756
+ return msgId;
580
757
  }
581
758
  const supMessages = (m, { direction = null, unread = false, limit = 200 } = {}) => m.db.prepare(`SELECT * FROM sup_messages WHERE 1=1 ${direction ? 'AND direction=?' : ''} ${unread ? 'AND read_at IS NULL' : ''} ORDER BY at DESC LIMIT ?`)
582
- .all(...(direction ? [direction] : []), limit).reverse();
583
- const markSupMessagesRead = (m, ids) => { let n = 0; for (const id of ids) { n += m.db.prepare('UPDATE sup_messages SET read_at=? WHERE msg_id=? AND read_at IS NULL').run(m.now(), id).changes; } return n; };
759
+ .all(...(direction ? [direction] : []), limit).reverse();
760
+ const markSupMessagesRead = (m, ids) => { let n = 0; for (const id of ids) {
761
+ n += m.db.prepare('UPDATE sup_messages SET read_at=? WHERE msg_id=? AND read_at IS NULL').run(m.now(), id).changes;
762
+ } return n; };
584
763
  function setSupSignal(m, { scope, key = 'main', value = null, token = null, holderPid = process.pid, expiresAt = null }) {
585
- return upsertRow(m.db, 'sup_signals', { scope, key, holder_pid: holderPid, token, value_json: value, at: m.now(), expires_at: expiresAt }, ['scope', 'key']);
764
+ return upsertRow(m.db, 'sup_signals', { scope, key, holder_pid: holderPid, token, value_json: value, at: m.now(), expires_at: expiresAt }, ['scope', 'key']);
586
765
  }
587
766
  const supSignal = (m, scope, key = 'main') => { const r = m.db.prepare('SELECT * FROM sup_signals WHERE scope=? AND key=?').get(scope, key); return r ? { ...r, value: parse(r.value_json) } : null; };
588
767
  const clearSupSignal = (m, scope, key = 'main') => m.db.prepare('DELETE FROM sup_signals WHERE scope=? AND key=?').run(scope, key).changes > 0;
589
768
  function recordMachineLlmUsage(m, { subjectType, supAttemptId = null, turnRef = null, spanId = null, provider, requestModel = null, responseModel = null, source = 'cli-transcript', ...counts }) {
590
- return Number(insertRow(m.db, 'llm_usage', { subject_type: subjectType, sup_attempt_id: supAttemptId, turn_ref: turnRef, span_id: spanId, provider, request_model: requestModel,
591
- response_model: responseModel, source, at: m.now(), ...snake(counts) }).lastInsertRowid);
769
+ return Number(insertRow(m.db, 'llm_usage', { subject_type: subjectType, sup_attempt_id: supAttemptId, turn_ref: turnRef, span_id: spanId, provider, request_model: requestModel,
770
+ response_model: responseModel, source, at: m.now(), ...snake(counts) }).lastInsertRowid);
592
771
  }
593
-
594
772
  // ---------------------------------------------------------------------------------------------------------------------
595
773
  // B2. Engine: process runs, leader, cursors, queue, schedules, actions, modes, SLA, invariants
596
774
  // ---------------------------------------------------------------------------------------------------------------------
597
775
  /** A long-lived runtime process starts (MB-04, G1). Returns run_id. */
598
776
  function startProcessRun(m, { role, pid = process.pid, rev = runtimeRev(), epoch = null, parentActionId = null, startReason = 'manual' }) {
599
- return Number(insertRow(m.db, 'process_runs', { role, pid, host: os.hostname(), rev, epoch, parent_action_id: parentActionId, start_reason: startReason,
600
- started_at: m.now(), last_heartbeat_at: m.now() }).lastInsertRowid);
777
+ return Number(insertRow(m.db, 'process_runs', { role, pid, host: os.hostname(), rev, epoch, parent_action_id: parentActionId, start_reason: startReason,
778
+ started_at: m.now(), last_heartbeat_at: m.now() }).lastInsertRowid);
601
779
  }
602
780
  const heartbeatProcessRun = (m, runId, { draining = null } = {}) => m.db.prepare('UPDATE process_runs SET last_heartbeat_at=?, draining_since=CASE WHEN ?=1 THEN COALESCE(draining_since,?) ELSE draining_since END WHERE run_id=? AND ended_at IS NULL')
603
- .run(m.now(), draining ? 1 : 0, m.now(), runId).changes > 0;
781
+ .run(m.now(), draining ? 1 : 0, m.now(), runId).changes > 0;
604
782
  /** A process run ends, once (trigger process_runs_end_once). exitReason: clean|reload-handover|crash|killed|lost-lease|stopped|unknown. */
605
783
  function endProcessRun(m, runId, { exitCode = null, exitReason, killedBy = null }) {
606
- const at = m.now();
607
- const row = m.db.prepare('SELECT last_heartbeat_at, ended_at FROM process_runs WHERE run_id=?').get(runId);
608
- if (!row || row.ended_at != null) return false;
609
- return m.db.prepare('UPDATE process_runs SET ended_at=?, exit_code=?, exit_reason=?, killed_by=?, heartbeat_age_at_end_ms=? WHERE run_id=? AND ended_at IS NULL')
610
- .run(at, exitCode, exitReason, killedBy, row.last_heartbeat_at != null ? at - row.last_heartbeat_at : null, runId).changes > 0;
784
+ const at = m.now();
785
+ const row = m.db.prepare('SELECT last_heartbeat_at, ended_at FROM process_runs WHERE run_id=?').get(runId);
786
+ if (!row || row.ended_at != null)
787
+ return false;
788
+ return m.db.prepare('UPDATE process_runs SET ended_at=?, exit_code=?, exit_reason=?, killed_by=?, heartbeat_age_at_end_ms=? WHERE run_id=? AND ended_at IS NULL')
789
+ .run(at, exitCode, exitReason, killedBy, row.last_heartbeat_at != null ? at - row.last_heartbeat_at : null, runId).changes > 0;
611
790
  }
612
791
  const openProcessRuns = (m, { role = null } = {}) => m.db.prepare(`SELECT * FROM process_runs WHERE ended_at IS NULL ${role ? 'AND role=?' : ''} ORDER BY run_id`).all(...(role ? [role] : []));
613
792
  const processRuns = (m, { role = null, sinceMs = null, limit = 200 } = {}) => m.db.prepare(`SELECT * FROM process_runs WHERE 1=1 ${role ? 'AND role=?' : ''} ${sinceMs != null ? 'AND started_at>?' : ''} ORDER BY run_id DESC LIMIT ?`)
614
- .all(...(role ? [role] : []), ...(sinceMs != null ? [m.now() - sinceMs] : []), limit);
615
-
793
+ .all(...(role ? [role] : []), ...(sinceMs != null ? [m.now() - sinceMs] : []), limit);
616
794
  const leaderOf = (m, name = 'reconciler') => m.db.prepare('SELECT * FROM engine_leader WHERE name=?').get(name) ?? null;
617
795
  /**
618
796
  * Acquire or renew the leader lease (fencing epoch). A fresh/expired lease is taken with a new epoch and a leader_history
619
797
  * row; renewal by the holder keeps the epoch. Returns {leader:boolean, epoch, row}.
620
798
  */
621
799
  function acquireLeader(m, { name = 'reconciler', holder, pid = process.pid, leaseMs, rev = runtimeRev(), processRunId = null, handover = false }) {
622
- return m.transaction((db) => {
623
- const at = m.now();
624
- const cur = db.prepare('SELECT * FROM engine_leader WHERE name=?').get(name);
625
- if (cur?.holder === holder && cur?.pid === pid) {
626
- db.prepare('UPDATE engine_leader SET heartbeat_at=?, expires_at=?, rev=?, process_run_id=COALESCE(?,process_run_id) WHERE name=?').run(at, at + leaseMs, rev, processRunId, name);
627
- return { leader: true, epoch: cur.epoch, renewed: true };
628
- }
629
- if (cur?.expires_at > at && !handover) return { leader: false, epoch: cur.epoch, holder: cur.holder, pid: cur.pid };
630
- const epoch = Math.max(Number(cur?.epoch ?? 0), Number(db.prepare('SELECT COALESCE(max(epoch),0) e FROM leader_history').get().e)) + 1;
631
- if (cur) db.prepare('UPDATE leader_history SET released_at=?, release_reason=? WHERE epoch=? AND released_at IS NULL').run(at, handover ? 'reload' : 'lost', cur.epoch);
632
- upsertRow(db, 'engine_leader', { name, holder, pid, epoch, process_run_id: processRunId, heartbeat_at: at, expires_at: at + leaseMs, rev, draining: 0, passes: 0, last_pass_ms: null, last_error: null }, ['name']);
633
- let how = 'fresh'; if (cur) how = handover ? 'handover' : 'takeover-stale';
634
- insertRow(db, 'leader_history', { epoch, holder, pid, process_run_id: processRunId, rev, acquired_at: at, acquired_how: how });
635
- return { leader: true, epoch, renewed: false };
636
- });
800
+ return m.transaction((db) => {
801
+ const at = m.now();
802
+ const cur = db.prepare('SELECT * FROM engine_leader WHERE name=?').get(name);
803
+ if (cur?.holder === holder && cur?.pid === pid) {
804
+ db.prepare('UPDATE engine_leader SET heartbeat_at=?, expires_at=?, rev=?, process_run_id=COALESCE(?,process_run_id) WHERE name=?').run(at, at + leaseMs, rev, processRunId, name);
805
+ return { leader: true, epoch: cur?.epoch, renewed: true };
806
+ }
807
+ if (cur?.expires_at > at && !handover)
808
+ return { leader: false, epoch: cur?.epoch, holder: cur?.holder, pid: cur?.pid };
809
+ const epoch = Math.max(Number(cur?.epoch ?? 0), Number(db.prepare('SELECT COALESCE(max(epoch),0) e FROM leader_history').get()?.e)) + 1;
810
+ if (cur)
811
+ db.prepare('UPDATE leader_history SET released_at=?, release_reason=? WHERE epoch=? AND released_at IS NULL').run(at, handover ? 'reload' : 'lost', cur.epoch);
812
+ upsertRow(db, 'engine_leader', { name, holder, pid, epoch, process_run_id: processRunId, heartbeat_at: at, expires_at: at + leaseMs, rev, draining: 0, passes: 0, last_pass_ms: null, last_error: null }, ['name']);
813
+ let how = 'fresh';
814
+ if (cur)
815
+ how = handover ? 'handover' : 'takeover-stale';
816
+ insertRow(db, 'leader_history', { epoch, holder, pid, process_run_id: processRunId, rev, acquired_at: at, acquired_how: how });
817
+ return { leader: true, epoch, renewed: false };
818
+ });
637
819
  }
638
820
  /** Renew only if still the holder at `epoch` (fence). Also records pass stats. */
639
821
  function renewLeader(m, { name = 'reconciler', epoch, leaseMs, passes = null, lastPassMs = null, lastError = undefined, draining = null }) {
640
- const at = m.now();
641
- return m.db.prepare(`UPDATE engine_leader SET heartbeat_at=?, expires_at=?, passes=COALESCE(?,passes), last_pass_ms=COALESCE(?,last_pass_ms)
822
+ const at = m.now();
823
+ return m.db.prepare(`UPDATE engine_leader SET heartbeat_at=?, expires_at=?, passes=COALESCE(?,passes), last_pass_ms=COALESCE(?,last_pass_ms)
642
824
  ${lastError !== undefined ? ', last_error=?' : ''} ${draining != null ? ', draining=?' : ''} WHERE name=? AND epoch=?`)
643
- .run(at, at + leaseMs, passes, lastPassMs, ...(lastError !== undefined ? [lastError] : []), ...(draining != null ? [draining ? 1 : 0] : []), name, epoch).changes > 0;
825
+ .run(at, at + leaseMs, passes, lastPassMs, ...(lastError !== undefined ? [lastError] : []), ...(draining != null ? [draining ? 1 : 0] : []), name, epoch).changes > 0;
644
826
  }
645
827
  /** Release the lease held at `epoch`; closes its leader_history row with the reason (reload|lost|killed|stop|crash). */
646
828
  function releaseLeader(m, { name = 'reconciler', epoch, reason = 'stop' }) {
647
- return m.transaction((db) => {
648
- const n = db.prepare('DELETE FROM engine_leader WHERE name=? AND epoch=?').run(name, epoch).changes;
649
- db.prepare('UPDATE leader_history SET released_at=?, release_reason=? WHERE epoch=? AND released_at IS NULL').run(m.now(), reason, epoch);
650
- return n > 0;
651
- });
829
+ return m.transaction((db) => {
830
+ const n = db.prepare('DELETE FROM engine_leader WHERE name=? AND epoch=?').run(name, epoch).changes;
831
+ db.prepare('UPDATE leader_history SET released_at=?, release_reason=? WHERE epoch=? AND released_at IS NULL').run(m.now(), reason, epoch);
832
+ return n > 0;
833
+ });
652
834
  }
653
835
  const leaderHistory = (m, { limit = 50 } = {}) => m.db.prepare('SELECT * FROM leader_history ORDER BY epoch DESC LIMIT ?').all(limit);
654
836
  const cursorOf = (m, ledgerId) => m.db.prepare('SELECT last_seq FROM engine_cursors WHERE ledger_id=?').get(ledgerId)?.last_seq ?? null;
655
837
  const setCursor = (m, ledgerId, lastSeq) => upsertRow(m.db, 'engine_cursors', { ledger_id: ledgerId, last_seq: lastSeq, updated_at: m.now() }, ['ledger_id']);
656
838
  const cursors = (m) => m.db.prepare('SELECT * FROM engine_cursors').all();
657
-
658
839
  function enqueue(m, { controller, key, dueAt = m.now(), reason = null }) {
659
- return m.db.prepare('INSERT INTO engine_queue(controller,key,due_at,reason) VALUES(?,?,?,?) ON CONFLICT(controller,key) DO UPDATE SET due_at=min(COALESCE(due_at,excluded.due_at),excluded.due_at), reason=excluded.reason')
660
- .run(controller, key, dueAt, reason);
840
+ return m.db.prepare('INSERT INTO engine_queue(controller,key,due_at,reason) VALUES(?,?,?,?) ON CONFLICT(controller,key) DO UPDATE SET due_at=min(COALESCE(due_at,excluded.due_at),excluded.due_at), reason=excluded.reason')
841
+ .run(controller, key, dueAt, reason);
661
842
  }
662
843
  const dueQueue = (m, { controller = null, limit = 100 } = {}) => m.db.prepare(`SELECT * FROM engine_queue WHERE due_at<=? ${controller ? 'AND controller=?' : ''} ORDER BY due_at LIMIT ?`)
663
- .all(m.now(), ...(controller ? [controller] : []), limit);
844
+ .all(m.now(), ...(controller ? [controller] : []), limit);
664
845
  const queueRows = (m) => m.db.prepare('SELECT * FROM engine_queue ORDER BY due_at').all();
665
846
  const dequeue = (m, controller, key) => m.db.prepare('DELETE FROM engine_queue WHERE controller=? AND key=?').run(controller, key).changes > 0;
666
847
  const requeue = (m, { controller, key, dueAt, error = null }) => m.db.prepare('UPDATE engine_queue SET due_at=?, tries=tries+1, last_error=? WHERE controller=? AND key=?').run(dueAt, error, controller, key).changes > 0;
667
-
668
848
  /** MB-01: the schedule row of a periodic duty; created with next_due_at = now + interval when absent (never "first run now"). */
669
849
  function ensureSchedule(m, { controller, duty, intervalMs, firstDueAt = null }) {
670
- m.db.prepare('INSERT INTO schedules(controller,duty,interval_ms,next_due_at) VALUES(?,?,?,?) ON CONFLICT(controller,duty) DO UPDATE SET interval_ms=excluded.interval_ms')
671
- .run(controller, duty, intervalMs, firstDueAt ?? m.now() + intervalMs);
672
- return m.db.prepare('SELECT * FROM schedules WHERE controller=? AND duty=?').get(controller, duty);
850
+ m.db.prepare('INSERT INTO schedules(controller,duty,interval_ms,next_due_at) VALUES(?,?,?,?) ON CONFLICT(controller,duty) DO UPDATE SET interval_ms=excluded.interval_ms')
851
+ .run(controller, duty, intervalMs, firstDueAt ?? m.now() + intervalMs);
852
+ return m.db.prepare('SELECT * FROM schedules WHERE controller=? AND duty=?').get(controller, duty);
673
853
  }
674
854
  /** Claim a due duty (no overlap): true when this caller should run it now. */
675
855
  /**
@@ -677,230 +857,243 @@ function ensureSchedule(m, { controller, duty, intervalMs, firstDueAt = null })
677
857
  * is older than `ttlMs` (default: the interval, at least 1 h), is released first, so a crashed run never blocks the duty.
678
858
  */
679
859
  function claimSchedule(m, { controller, duty, actionId = null, pid = process.pid, ttlMs = null, alive = pidAlive }) {
680
- return m.transaction((db) => {
681
- const at = m.now();
682
- const row = db.prepare('SELECT * FROM schedules WHERE controller=? AND duty=?').get(controller, duty);
683
- if (!row) return false;
684
- if (row.running_pid != null && row.running_pid !== pid) {
685
- const ttl = ttlMs ?? Math.max(row.interval_ms, 3600000);
686
- if (!alive(row.running_pid) || at - (row.last_started_at ?? 0) > ttl)
687
- db.prepare("UPDATE schedules SET running_pid=NULL, last_result='unknown', last_finished_at=? WHERE controller=? AND duty=? AND running_pid=?").run(at, controller, duty, row.running_pid);
688
- }
689
- return db.prepare('UPDATE schedules SET last_started_at=?, running_pid=?, last_action_id=COALESCE(?,last_action_id) WHERE controller=? AND duty=? AND next_due_at<=? AND (running_pid IS NULL OR running_pid=?)')
690
- .run(at, pid, actionId, controller, duty, at, pid).changes > 0;
691
- });
860
+ return m.transaction((db) => {
861
+ const at = m.now();
862
+ const row = db.prepare('SELECT * FROM schedules WHERE controller=? AND duty=?').get(controller, duty);
863
+ if (!row)
864
+ return false;
865
+ if (row.running_pid != null && row.running_pid !== pid) {
866
+ const ttl = ttlMs ?? Math.max(row.interval_ms, 3600000);
867
+ if (!alive(row.running_pid) || at - (row.last_started_at ?? 0) > ttl)
868
+ db.prepare("UPDATE schedules SET running_pid=NULL, last_result='unknown', last_finished_at=? WHERE controller=? AND duty=? AND running_pid=?").run(at, controller, duty, row.running_pid);
869
+ }
870
+ return db.prepare('UPDATE schedules SET last_started_at=?, running_pid=?, last_action_id=COALESCE(?,last_action_id) WHERE controller=? AND duty=? AND next_due_at<=? AND (running_pid IS NULL OR running_pid=?)')
871
+ .run(at, pid, actionId, controller, duty, at, pid).changes > 0;
872
+ });
692
873
  }
693
874
  function finishSchedule(m, { controller, duty, result = 'done', digest = null, nextDueAt = null }) {
694
- const at = m.now();
695
- const row = m.db.prepare('SELECT interval_ms FROM schedules WHERE controller=? AND duty=?').get(controller, duty);
696
- if (!row) return false;
697
- return m.db.prepare('UPDATE schedules SET last_finished_at=?, last_result=?, last_result_digest=?, next_due_at=?, running_pid=NULL WHERE controller=? AND duty=?')
698
- .run(at, result, digest, nextDueAt ?? at + row.interval_ms, controller, duty).changes > 0;
875
+ const at = m.now();
876
+ const row = m.db.prepare('SELECT interval_ms FROM schedules WHERE controller=? AND duty=?').get(controller, duty);
877
+ if (!row)
878
+ return false;
879
+ return m.db.prepare('UPDATE schedules SET last_finished_at=?, last_result=?, last_result_digest=?, next_due_at=?, running_pid=NULL WHERE controller=? AND duty=?')
880
+ .run(at, result, digest, nextDueAt ?? at + row.interval_ms, controller, duty).changes > 0;
699
881
  }
700
882
  const schedules = (m) => m.db.prepare('SELECT * FROM schedules ORDER BY controller, duty').all();
701
-
702
883
  /** Deterministic action id = sha(controller, key, verb, epoch, observed_generation). */
703
884
  const actionIdOf = ({ controller, key = '', verb = '', epoch = 0, observedGeneration = 0 }) => sha256([controller, key, verb, epoch, observedGeneration].join('\u0000')).slice(0, 32);
704
885
  /** Record an action intent (idempotent on id). */
705
- function actionIntent(m, { id = null, controller, duty = null, key = null, verb = null, argvDigest = null, epoch = null, observedGeneration = null, spanId = newSpanId(), traceId = null,
706
- mode = null, ledgerId = null, workflowId = null, jobId = null, attemptId = null }) {
707
- const actionId = id ?? actionIdOf({ controller, key, verb, epoch, observedGeneration });
708
- insertRow(m.db, 'engine_actions', { id: actionId, controller, duty, key, verb, argv_digest: argvDigest, epoch, observed_generation: observedGeneration, span_id: spanId, trace_id: traceId,
709
- state: 'intent', mode, ledger_id: ledgerId, workflow_id: workflowId, job_id: jobId, attempt_id: attemptId }, { orIgnore: true });
710
- return actionId;
886
+ function actionIntent(m, { id = null, controller, duty = null, key = null, verb = null, argvDigest = null, epoch = null, observedGeneration = null, spanId = newSpanId(), traceId = null, mode = null, ledgerId = null, workflowId = null, jobId = null, attemptId = null }) {
887
+ const actionId = id ?? actionIdOf({ controller, key, verb, epoch, observedGeneration });
888
+ insertRow(m.db, 'engine_actions', { id: actionId, controller, duty, key, verb, argv_digest: argvDigest, epoch, observed_generation: observedGeneration, span_id: spanId, trace_id: traceId,
889
+ state: 'intent', mode, ledger_id: ledgerId, workflow_id: workflowId, job_id: jobId, attempt_id: attemptId }, { orIgnore: true });
890
+ return actionId;
711
891
  }
712
892
  const actionRunning = (m, id, { requestId = null, childRunId = null } = {}) => m.db.prepare("UPDATE engine_actions SET state='running', started_at=COALESCE(started_at,?), request_id=COALESCE(?,request_id), child_run_id=COALESCE(?,child_run_id) WHERE id=? AND state IN ('intent','running')")
713
- .run(m.now(), requestId, childRunId, id).changes > 0;
893
+ .run(m.now(), requestId, childRunId, id).changes > 0;
714
894
  /**
715
895
  * Finish an action (MB-03): `result` is kept IN FULL as a blob (result_sha); result_json holds only a ≤8 KiB summary.
716
896
  * stdout/stderr are stored in full as blobs.
717
897
  */
718
898
  function actionFinish(m, id, { state = 'done', exitCode = null, result: rawResult = null, summary: rawSummary = null, stdout = null, stderr = null, errorSignature = null }) {
719
- const result = rawResult == null ? null : redactData(rawResult);
720
- const summary = rawSummary == null ? null : redactData(rawSummary);
721
- let full = null; if (result != null) full = typeof result === 'string' ? result : JSON.stringify(result);
722
- const resultSha = full == null ? null : putMachineBlob(m, full, { mediaType: typeof result === 'string' ? 'text/plain' : 'application/json' });
723
- let brief = summary ?? (full != null && Buffer.byteLength(full) <= 8192 && typeof result !== 'string' ? result : null);
724
- if (brief != null) { const s = JSON.stringify(brief); if (Buffer.byteLength(s) > 8192) brief = { truncated: true, bytes: Buffer.byteLength(s), sha256: resultSha }; }
725
- const stdoutSha = textBlob(m, stdout), stderrSha = textBlob(m, stderr);
726
- return m.db.prepare('UPDATE engine_actions SET state=?, finished_at=?, started_at=COALESCE(started_at,?), exit_code=?, result_json=?, result_sha=?, stdout_sha=?, stderr_sha=?, error_signature=? WHERE id=?')
727
- .run(state, m.now(), m.now(), exitCode, brief == null ? null : JSON.stringify(brief), resultSha, stdoutSha, stderrSha, errorSignature, id).changes > 0;
899
+ const result = rawResult == null ? null : redactData(rawResult);
900
+ const summary = rawSummary == null ? null : redactData(rawSummary);
901
+ let full = null;
902
+ if (result != null)
903
+ full = typeof result === 'string' ? result : JSON.stringify(result);
904
+ const resultSha = full == null ? null : putMachineBlob(m, full, { mediaType: typeof result === 'string' ? 'text/plain' : 'application/json' });
905
+ let brief = summary ?? (full != null && Buffer.byteLength(full) <= 8192 && typeof result !== 'string' ? result : null);
906
+ if (brief != null) {
907
+ const s = JSON.stringify(brief);
908
+ if (Buffer.byteLength(s) > 8192)
909
+ brief = { truncated: true, bytes: Buffer.byteLength(s), sha256: resultSha };
910
+ }
911
+ const stdoutSha = textBlob(m, stdout), stderrSha = textBlob(m, stderr);
912
+ return m.db.prepare('UPDATE engine_actions SET state=?, finished_at=?, started_at=COALESCE(started_at,?), exit_code=?, result_json=?, result_sha=?, stdout_sha=?, stderr_sha=?, error_signature=? WHERE id=?')
913
+ .run(state, m.now(), m.now(), exitCode, brief == null ? null : JSON.stringify(brief), resultSha, stdoutSha, stderrSha, errorSignature, id).changes > 0;
728
914
  }
729
915
  /** Actions left in intent/running by a dead engine become unknown (never replayed). */
730
916
  const markStaleActionsUnknown = (m, { olderThanMs = 0, exceptEpoch = null } = {}) => m.db.prepare(`UPDATE engine_actions SET state='unknown', finished_at=? WHERE state IN ('intent','running') AND COALESCE(started_at,0)<=? ${exceptEpoch != null ? 'AND COALESCE(epoch,-1)<>?' : ''}`)
731
- .run(m.now(), m.now() - olderThanMs, ...(exceptEpoch != null ? [exceptEpoch] : [])).changes;
917
+ .run(m.now(), m.now() - olderThanMs, ...(exceptEpoch != null ? [exceptEpoch] : [])).changes;
732
918
  const actionOf = (m, id) => { const r = m.db.prepare('SELECT * FROM engine_actions WHERE id=?').get(id); return r ? { ...r, result: parse(r.result_json) } : null; };
733
919
  function actions(m, { controller = null, state = null, sinceMs = null, limit = 200 } = {}) {
734
- const where = [], args = [];
735
- if (controller) { where.push('controller=?'); args.push(controller); }
736
- if (state) { where.push('state=?'); args.push(state); }
737
- if (sinceMs != null) { where.push('COALESCE(finished_at,started_at)>?'); args.push(m.now() - sinceMs); }
738
- return m.db.prepare(`SELECT * FROM engine_actions ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY COALESCE(finished_at,started_at) DESC LIMIT ?`).all(...args, limit)
739
- .map((r) => ({ ...r, result: parse(r.result_json) }));
920
+ const where = [], args = [];
921
+ if (controller) {
922
+ where.push('controller=?');
923
+ args.push(controller);
924
+ }
925
+ if (state) {
926
+ where.push('state=?');
927
+ args.push(state);
928
+ }
929
+ if (sinceMs != null) {
930
+ where.push('COALESCE(finished_at,started_at)>?');
931
+ args.push(m.now() - sinceMs);
932
+ }
933
+ return m.db.prepare(`SELECT * FROM engine_actions ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY COALESCE(finished_at,started_at) DESC LIMIT ?`).all(...args, limit)
934
+ .map((r) => ({ ...r, result: parse(r.result_json) }));
740
935
  }
741
936
  function actionStep(m, actionId, { step, ms = null, ok = null, detail = null }) {
742
- const n = Number(m.db.prepare('SELECT COALESCE(max(step_no),0)+1 n FROM action_steps WHERE action_id=?').get(actionId).n);
743
- insertRow(m.db, 'action_steps', { action_id: actionId, step_no: n, step, started_at: m.now(), ms, ok: bool(ok), detail: detail == null ? null : String(detail) });
744
- return n;
937
+ const n = Number(m.db.prepare('SELECT COALESCE(max(step_no),0)+1 n FROM action_steps WHERE action_id=?').get(actionId)?.n);
938
+ insertRow(m.db, 'action_steps', { action_id: actionId, step_no: n, step, started_at: m.now(), ms, ok: bool(ok), detail: detail == null ? null : String(detail) });
939
+ return n;
745
940
  }
746
-
747
941
  const controllerModes = (m) => Object.fromEntries(m.db.prepare('SELECT controller, mode FROM controller_modes').all().map((r) => [r.controller, r.mode]));
748
942
  /** Change a controller mode: a mode_changes row first (who and why, G6), then controller_modes (trigger enforces the order). */
749
943
  function setControllerMode(m, { controller, mode, by, reason }) {
750
- need(by && reason, 'setControllerMode needs by and reason');
751
- return m.transaction((db) => {
752
- const from = db.prepare('SELECT mode FROM controller_modes WHERE controller=?').get(controller)?.mode ?? null;
753
- if (from === mode) return { changed: false, mode };
754
- insertRow(db, 'mode_changes', { controller, from_mode: from, to_mode: mode, by, reason, at: m.now() });
755
- upsertRow(db, 'controller_modes', { controller, mode, set_at: m.now(), set_by: by }, ['controller']);
756
- return { changed: true, from, mode };
757
- });
944
+ need(by && reason, 'setControllerMode needs by and reason');
945
+ return m.transaction((db) => {
946
+ const from = db.prepare('SELECT mode FROM controller_modes WHERE controller=?').get(controller)?.mode ?? null;
947
+ if (from === mode)
948
+ return { changed: false, mode };
949
+ insertRow(db, 'mode_changes', { controller, from_mode: from, to_mode: mode, by, reason, at: m.now() });
950
+ upsertRow(db, 'controller_modes', { controller, mode, set_at: m.now(), set_by: by }, ['controller']);
951
+ return { changed: true, from, mode };
952
+ });
758
953
  }
759
954
  const modeChanges = (m, { limit = 100 } = {}) => m.db.prepare('SELECT * FROM mode_changes ORDER BY change_id DESC LIMIT ?').all(limit);
760
-
761
955
  /** Open an SLA episode for (entity,state) unless one is open (G3). Returns the episode id. */
762
956
  function openSlaEpisode(m, { entity, state, code, severity = 'warn', ledgerId = null, workflowId = null, slaMs, enteredAt = m.now() }) {
763
- const open = m.db.prepare('SELECT episode_id FROM sla_episodes WHERE entity=? AND state=? AND cleared_at IS NULL').get(entity, state);
764
- if (open) return Number(open.episode_id);
765
- return Number(insertRow(m.db, 'sla_episodes', { entity, state, code, severity, ledger_id: ledgerId, workflow_id: workflowId, entered_at: enteredAt, sla_ms: slaMs }).lastInsertRowid);
957
+ const open = m.db.prepare('SELECT episode_id FROM sla_episodes WHERE entity=? AND state=? AND cleared_at IS NULL').get(entity, state);
958
+ if (open)
959
+ return Number(open.episode_id);
960
+ return Number(insertRow(m.db, 'sla_episodes', { entity, state, code, severity, ledger_id: ledgerId, workflow_id: workflowId, entered_at: enteredAt, sla_ms: slaMs }).lastInsertRowid);
766
961
  }
767
962
  const markSlaViolated = (m, episodeId) => m.db.prepare('UPDATE sla_episodes SET violated_at=? WHERE episode_id=? AND violated_at IS NULL').run(m.now(), episodeId).changes > 0;
768
963
  const markSlaReported = (m, episodeId, { diId = null } = {}) => m.db.prepare('UPDATE sla_episodes SET reported_at=?, di_id=COALESCE(di_id,?) WHERE episode_id=? AND reported_at IS NULL').run(m.now(), diId, episodeId).changes > 0;
769
964
  function clearSla(m, { entity, state = null, reason = 'resolved' }) {
770
- return m.db.prepare(`UPDATE sla_episodes SET cleared_at=?, clear_reason=? WHERE entity=? ${state ? 'AND state=?' : ''} AND cleared_at IS NULL`).run(m.now(), reason, entity, ...(state ? [state] : [])).changes;
965
+ return m.db.prepare(`UPDATE sla_episodes SET cleared_at=?, clear_reason=? WHERE entity=? ${state ? 'AND state=?' : ''} AND cleared_at IS NULL`).run(m.now(), reason, entity, ...(state ? [state] : [])).changes;
771
966
  }
772
967
  const openSla = (m) => m.db.prepare('SELECT * FROM v_sla_open ORDER BY entered_at').all();
773
968
  function recordViolation(m, { code, severity = 'warn', entity, ledgerId = null, workflowId = null, episodeId = null, diId = null, lessonId = null, detail = null }) {
774
- return Number(insertRow(m.db, 'invariant_violations', { code, severity, entity, ledger_id: ledgerId, workflow_id: workflowId, episode_id: episodeId, violated_at: m.now(), di_id: diId, lesson_id: lessonId, detail_json: detail }).lastInsertRowid);
969
+ return Number(insertRow(m.db, 'invariant_violations', { code, severity, entity, ledger_id: ledgerId, workflow_id: workflowId, episode_id: episodeId, violated_at: m.now(), di_id: diId, lesson_id: lessonId, detail_json: detail }).lastInsertRowid);
775
970
  }
776
971
  const clearViolation = (m, violationId) => m.db.prepare('UPDATE invariant_violations SET cleared_at=? WHERE violation_id=? AND cleared_at IS NULL').run(m.now(), violationId).changes > 0;
777
-
778
972
  // ---------------------------------------------------------------------------------------------------------------------
779
973
  // B3. Services, seats, deliveries, terminals, locks, claims, sessions
780
974
  // ---------------------------------------------------------------------------------------------------------------------
781
975
  /** Set a service's state; a change appends service_events (G5). */
782
976
  function setService(m, { name, kind, state, pid = undefined, port = undefined, url = undefined, probe = undefined, quarantinedUntil = undefined, action = null, actionId = null, probeMs = null, probeError = null }) {
783
- return m.transaction((db) => {
784
- const cur = db.prepare('SELECT state FROM services WHERE name=?').get(name);
785
- const at = m.now();
786
- upsertRow(db, 'services', { name, kind, state, since: cur?.state === state ? undefined : at, pid, port, url, last_probe_json: probe, quarantined_until: quarantinedUntil }, ['name']);
787
- if (cur?.state !== state || (action && action !== 'none'))
788
- insertRow(db, 'service_events', { name, at, from_state: cur?.state ?? null, to_state: state, probe_ms: probeMs, probe_error: probeError, action, action_id: actionId });
789
- return { changed: cur?.state !== state, from: cur?.state ?? null };
790
- });
977
+ return m.transaction((db) => {
978
+ const cur = db.prepare('SELECT state FROM services WHERE name=?').get(name);
979
+ const at = m.now();
980
+ upsertRow(db, 'services', { name, kind, state, since: cur?.state === state ? undefined : at, pid, port, url, last_probe_json: probe, quarantined_until: quarantinedUntil }, ['name']);
981
+ if (cur?.state !== state || (action && action !== 'none'))
982
+ insertRow(db, 'service_events', { name, at, from_state: cur?.state ?? null, to_state: state, probe_ms: probeMs, probe_error: probeError, action, action_id: actionId });
983
+ return { changed: cur?.state !== state, from: cur?.state ?? null };
984
+ });
791
985
  }
792
986
  const recordProbe = (m, { name, ok, latencyMs = null, detail = null }) => insertRow(m.db, 'service_probes', { name, at: m.now(), ok: ok ? 1 : 0, latency_ms: latencyMs, detail_json: detail });
793
987
  const services = (m) => m.db.prepare('SELECT * FROM v_services ORDER BY name').all().map((r) => ({ ...r, lastProbe: parse(r.last_probe_json) }));
794
988
  const serviceEvents = (m, { name = null, sinceMs = 86400000 } = {}) => m.db.prepare(`SELECT * FROM service_events WHERE at>? ${name ? 'AND name=?' : ''} ORDER BY seq`).all(m.now() - sinceMs, ...(name ? [name] : []));
795
-
796
989
  const upsertSeat = (m, seat) => upsertRow(m.db, 'seats', snake(seat), ['seat_id']);
797
990
  const seatOf = (m, seatId) => { const r = m.db.prepare('SELECT * FROM seats WHERE seat_id=?').get(seatId); return r ? { ...r, detail: parse(r.detail_json) } : null; };
798
991
  const seats = (m) => m.db.prepare('SELECT * FROM v_seats ORDER BY seat_id').all();
799
992
  /** One delivery attempt to a seat (G11, MB-02); the trigger counts consecutive input failures on the seat. */
800
993
  function recordDelivery(m, { messageKind, messageRef, ledgerId = null, seatId = null, terminalHandle = null, channel, outcome, turnId = null, detail = null }) {
801
- return Number(insertRow(m.db, 'deliveries', { message_kind: messageKind, message_ref: String(messageRef), ledger_id: ledgerId, seat_id: seatId, terminal_handle: terminalHandle,
802
- channel, attempted_at: m.now(), outcome, turn_id: turnId, detail: detail == null ? null : String(detail) }).lastInsertRowid);
994
+ return Number(insertRow(m.db, 'deliveries', { message_kind: messageKind, message_ref: String(messageRef), ledger_id: ledgerId, seat_id: seatId, terminal_handle: terminalHandle,
995
+ channel, attempted_at: m.now(), outcome, turn_id: turnId, detail: detail == null ? null : String(detail) }).lastInsertRowid);
803
996
  }
804
997
  /** A wake outcome (scripts/kernel/wake-delivery.mjs action) as a deliveries.outcome, or null when it says nothing about input. */
805
998
  const WAKE_OUTCOMES = Object.freeze({ 'kernel-woken': 'delivered', 'kernel-unwritable': 'unwritable', 'kernel-exited': 'exited', 'kernel-unavailable': 'unavailable',
806
- 'kernel-send-failed': 'failed', 'kernel-busy': 'busy-deferred' });
999
+ 'kernel-send-failed': 'failed', 'kernel-busy': 'busy-deferred' });
807
1000
  /**
808
1001
  * MB-05: count one wake of a seat. The outcome becomes a deliveries row; the deliveries trigger keeps
809
1002
  * seats.input_failures_consecutive / _total (a refused input adds one, a delivered one resets). A seat whose
810
1003
  * terminal changed starts from zero. Returns {failures, since, replace} with replace at `max` in a row.
811
1004
  */
812
1005
  function recordSeatInput(m, { seatId, terminal = null, action, messageKind = 'wake', messageRef = 'wake', channel = 'orca', role = 'supervisor', max = 3, detail = null }) {
813
- return m.transaction((db) => {
814
- const seat = db.prepare('SELECT * FROM seats WHERE seat_id=?').get(seatId);
815
- if (!seat) insertRow(db, 'seats', { seat_id: seatId, role, state: 'live', terminal_handle: terminal });
816
- else if (terminal && seat.terminal_handle !== terminal)
817
- db.prepare('UPDATE seats SET terminal_handle=?, input_failures_consecutive=0, last_input_failure_at=NULL WHERE seat_id=?').run(terminal, seatId);
818
- const outcome = WAKE_OUTCOMES[action] ?? null;
819
- // A busy seat is not an input answer: its row carries no seat_id, so the trigger leaves the run of failures as it is.
820
- if (outcome) recordDelivery(m, { messageKind, messageRef, seatId: outcome === 'busy-deferred' ? null : seatId, terminalHandle: terminal, channel, outcome, detail: detail ?? `${seatId} ${action}` });
821
- const row = db.prepare('SELECT input_failures_consecutive n, last_input_ok_at ok, last_input_failure_at bad FROM seats WHERE seat_id=?').get(seatId);
822
- const failures = Number(row.n) || 0;
823
- const since = failures ? db.prepare("SELECT min(attempted_at) at FROM (SELECT attempted_at FROM deliveries WHERE seat_id=? AND outcome IN ('unwritable','exited','unavailable','failed') ORDER BY delivery_id DESC LIMIT ?)").get(seatId, failures)?.at ?? row.bad : null;
824
- return { failures, since, replace: failures >= max };
825
- });
1006
+ return m.transaction((db) => {
1007
+ const seat = db.prepare('SELECT * FROM seats WHERE seat_id=?').get(seatId);
1008
+ if (!seat)
1009
+ insertRow(db, 'seats', { seat_id: seatId, role, state: 'live', terminal_handle: terminal });
1010
+ else if (terminal && seat.terminal_handle !== terminal)
1011
+ db.prepare('UPDATE seats SET terminal_handle=?, input_failures_consecutive=0, last_input_failure_at=NULL WHERE seat_id=?').run(terminal, seatId);
1012
+ const outcome = WAKE_OUTCOMES[action] ?? null;
1013
+ // A busy seat is not an input answer: its row carries no seat_id, so the trigger leaves the run of failures as it is.
1014
+ if (outcome)
1015
+ recordDelivery(m, { messageKind, messageRef, seatId: outcome === 'busy-deferred' ? null : seatId, terminalHandle: terminal, channel, outcome, detail: detail ?? `${seatId} ${action}` });
1016
+ const row = db.prepare('SELECT input_failures_consecutive n, last_input_ok_at ok, last_input_failure_at bad FROM seats WHERE seat_id=?').get(seatId);
1017
+ const failures = Number(row?.n) || 0;
1018
+ const since = failures ? db.prepare("SELECT min(attempted_at) at FROM (SELECT attempted_at FROM deliveries WHERE seat_id=? AND outcome IN ('unwritable','exited','unavailable','failed') ORDER BY delivery_id DESC LIMIT ?)").get(seatId, failures)?.at ?? row?.bad : null;
1019
+ return { failures, since, replace: failures >= max };
1020
+ });
826
1021
  }
827
1022
  const startSeatTurn = (m, { seatId, wokenByDelivery = null, spanId = newSpanId() }) => Number(insertRow(m.db, 'seat_turns', { seat_id: seatId, woken_by_delivery: wokenByDelivery, started_at: m.now(), span_id: spanId }).lastInsertRowid);
828
1023
  const endSeatTurn = (m, turnId, { endReason = 'idle', actionsCount = null } = {}) => m.db.prepare('UPDATE seat_turns SET ended_at=?, end_reason=?, actions_count=COALESCE(?,actions_count) WHERE turn_id=? AND ended_at IS NULL').run(m.now(), endReason, actionsCount, turnId).changes > 0;
829
1024
  function seatTranscriptSnapshot(m, { seatId, terminalHandle = null, text }) {
830
- const sha = putMachineBlob(m, String(text), { mediaType: 'text/plain' });
831
- insertRow(m.db, 'seat_transcript_snapshots', { seat_id: seatId, terminal_handle: terminalHandle, at: m.now(), lines: String(text).split('\n').length, bytes: Buffer.byteLength(String(text)), sha256: sha }, { orIgnore: true });
832
- return sha;
1025
+ const sha = putMachineBlob(m, String(text), { mediaType: 'text/plain' });
1026
+ insertRow(m.db, 'seat_transcript_snapshots', { seat_id: seatId, terminal_handle: terminalHandle, at: m.now(), lines: String(text).split('\n').length, bytes: Buffer.byteLength(String(text)), sha256: sha }, { orIgnore: true });
1027
+ return sha;
833
1028
  }
834
1029
  const upsertTerminal = (m, terminal) => upsertRow(m.db, 'terminals', snake(terminal), ['handle']);
835
1030
  const closeTerminal = (m, handle, { by = null, verified = false } = {}) => m.db.prepare('UPDATE terminals SET closed_at=COALESCE(closed_at,?), closed_by=COALESCE(closed_by,?), close_verified_at=CASE WHEN ? THEN ? ELSE close_verified_at END WHERE handle=?')
836
- .run(m.now(), by, verified ? 1 : 0, m.now(), handle).changes > 0;
837
-
1031
+ .run(m.now(), by, verified ? 1 : 0, m.now(), handle).changes > 0;
838
1032
  /**
839
1033
  * Take a host lock (replaces connectors/*.lock and *.starting.json): succeeds when free, released, expired, or already
840
1034
  * ours. Returns {ok, holder?}.
841
1035
  */
842
1036
  function acquireHostLock(m, { name, holder = null, pid = process.pid, ttlMs = 60000, processRunId = null, state = 'held', retainExpired = false }) {
843
- return m.transaction((db) => {
844
- const at = m.now();
845
- const cur = db.prepare('SELECT * FROM host_locks WHERE name=?').get(name);
846
- if (cur && cur.state !== 'released' && (cur.expires_at > at || (retainExpired && name.startsWith('provision:'))) && (cur.holder_pid !== pid || (retainExpired && name.startsWith('provision:') && cur.holder !== holder))) return { ok: false, holder: cur };
847
- upsertRow(db, 'host_locks', { name, holder_pid: pid, holder, process_run_id: processRunId, started_at: cur && cur.holder_pid === pid && cur.state !== 'released' ? cur.started_at : at,
848
- heartbeat_at: at, expires_at: at + ttlMs, handed_over_from: cur && cur.holder_pid !== pid && cur.state !== 'released' ? cur.holder_pid : null, state }, ['name']);
849
- return { ok: true };
850
- });
1037
+ return m.transaction((db) => {
1038
+ const at = m.now();
1039
+ const cur = db.prepare('SELECT * FROM host_locks WHERE name=?').get(name);
1040
+ if (cur && cur.state !== 'released' && (cur.expires_at > at || (retainExpired && name.startsWith('provision:'))) && (cur.holder_pid !== pid || (retainExpired && name.startsWith('provision:') && cur.holder !== holder)))
1041
+ return { ok: false, holder: cur };
1042
+ upsertRow(db, 'host_locks', { name, holder_pid: pid, holder, process_run_id: processRunId, started_at: cur && cur.holder_pid === pid && cur.state !== 'released' ? cur.started_at : at,
1043
+ heartbeat_at: at, expires_at: at + ttlMs, handed_over_from: cur && cur.holder_pid !== pid && cur.state !== 'released' ? cur.holder_pid : null, state }, ['name']);
1044
+ return { ok: true };
1045
+ });
851
1046
  }
852
1047
  const renewHostLock = (m, { name, pid = process.pid, ttlMs = 60000, state = undefined }) => m.db.prepare(`UPDATE host_locks SET heartbeat_at=?, expires_at=? ${state ? ', state=?' : ''} WHERE name=? AND holder_pid=? AND state<>'released'`)
853
- .run(m.now(), m.now() + ttlMs, ...(state ? [state] : []), name, pid).changes > 0;
1048
+ .run(m.now(), m.now() + ttlMs, ...(state ? [state] : []), name, pid).changes > 0;
854
1049
  const releaseHostLock = (m, { name, pid = process.pid, force = false }) => m.db.prepare(`UPDATE host_locks SET state='released', heartbeat_at=? WHERE name=? ${force ? '' : 'AND holder_pid=?'} AND state<>'released'`)
855
- .run(m.now(), name, ...(force ? [] : [pid])).changes > 0;
1050
+ .run(m.now(), name, ...(force ? [] : [pid])).changes > 0;
856
1051
  const hostLock = (m, name) => m.db.prepare('SELECT * FROM host_locks WHERE name=?').get(name) ?? null;
857
1052
  const hostLocks = (m) => m.db.prepare("SELECT * FROM host_locks WHERE state<>'released' ORDER BY name").all();
858
-
859
1053
  function claimResource(m, { resourcePath, kind, ownerActionId = null, ownerPid = process.pid, ownerRunId = null, hasJunctions = false }) {
860
- return Number(insertRow(m.db, 'claims', { resource_path: path.resolve(resourcePath), kind, owner_action_id: ownerActionId, owner_pid: ownerPid, owner_run_id: ownerRunId,
861
- has_junctions: hasJunctions ? 1 : 0, created_at: m.now() }).lastInsertRowid);
1054
+ return Number(insertRow(m.db, 'claims', { resource_path: path.resolve(resourcePath), kind, owner_action_id: ownerActionId, owner_pid: ownerPid, owner_run_id: ownerRunId,
1055
+ has_junctions: hasJunctions ? 1 : 0, created_at: m.now() }).lastInsertRowid);
862
1056
  }
863
1057
  const releaseClaim = (m, claimId) => m.db.prepare('UPDATE claims SET released_at=? WHERE claim_id=? AND released_at IS NULL').run(m.now(), claimId).changes > 0;
864
1058
  const sweptClaim = (m, claimId, { error = null } = {}) => m.db.prepare('UPDATE claims SET swept_at=?, sweep_error=? WHERE claim_id=?').run(m.now(), error, claimId).changes > 0;
865
1059
  const liveClaims = (m) => m.db.prepare('SELECT * FROM claims WHERE released_at IS NULL AND swept_at IS NULL ORDER BY claim_id').all();
866
1060
  const upsertAgentSession = (m, session) => upsertRow(m.db, 'agent_sessions', snake(session), ['session_id']);
867
-
868
1061
  // ---------------------------------------------------------------------------------------------------------------------
869
1062
  // B4. Resources: throttle, samples, providers, pool backoff, quotas, guards, host leases, budgets
870
1063
  // ---------------------------------------------------------------------------------------------------------------------
871
1064
  const throttleState = (m) => { const r = m.db.prepare('SELECT * FROM throttle_state WHERE id=1').get(); return r ? { ...r, slotTargets: parse(r.slot_targets_json), priorities: parse(r.priorities_json) } : null; };
872
1065
  /** Write the current throttle row (MB-15: writer + rev). A mode change appends throttle_events first (G7). */
873
- function setThrottle(m, { mode, effectiveCap = null, heavyCap = null, running = null, freeRamPct = null, freeRamMb = null, cpuPct = null, cpuHot = null, reason = null,
874
- writer, writerRev = runtimeRev(), slotTargets = null, priorities = null, sample = null }) {
875
- need(writer, 'setThrottle needs writer');
876
- return m.transaction((db) => {
877
- const at = m.now();
878
- const cur = db.prepare('SELECT mode, since, writer, writer_rev FROM throttle_state WHERE id=1').get();
879
- // MB-15: a process running an OLDER runtime never overwrites what a newer runtime wrote.
880
- if (cur && compareRevs(writerRev, cur.writer_rev) < 0)
881
- return { changed: false, refused: `stale-writer-rev: ${writerRev} is older than ${cur.writer_rev} (${cur.writer})`, from: cur.mode };
882
- const changed = cur?.mode !== mode;
883
- if (changed) insertRow(db, 'throttle_events', { at, from_mode: cur?.mode ?? null, to_mode: mode, reason, free_ram_pct: freeRamPct, cpu_pct: cpuPct, effective_cap: effectiveCap, running, writer_rev: writerRev, sample_json: sample });
884
- upsertRow(db, 'throttle_state', { id: 1, mode, effective_cap: effectiveCap, heavy_cap: heavyCap, running, free_ram_pct: freeRamPct, free_ram_mb: freeRamMb, cpu_pct: cpuPct, cpu_hot: int(cpuHot),
885
- since: changed ? at : cur.since, updated_at: at, reason, writer, writer_rev: writerRev, slot_targets_json: slotTargets, priorities_json: priorities }, ['id']);
886
- return { changed, from: cur?.mode ?? null };
887
- });
1066
+ function setThrottle(m, { mode, effectiveCap = null, heavyCap = null, running = null, freeRamPct = null, freeRamMb = null, cpuPct = null, cpuHot = null, reason = null, writer, writerRev = runtimeRev(), slotTargets = null, priorities = null, sample = null }) {
1067
+ need(writer, 'setThrottle needs writer');
1068
+ return m.transaction((db) => {
1069
+ const at = m.now();
1070
+ const cur = db.prepare('SELECT mode, since, writer, writer_rev FROM throttle_state WHERE id=1').get();
1071
+ // MB-15: a process running an OLDER runtime never overwrites what a newer runtime wrote.
1072
+ if (cur && compareRevs(writerRev, cur.writer_rev) < 0)
1073
+ return { changed: false, refused: `stale-writer-rev: ${writerRev} is older than ${cur.writer_rev} (${cur.writer})`, from: cur.mode };
1074
+ const changed = cur?.mode !== mode;
1075
+ if (changed)
1076
+ insertRow(db, 'throttle_events', { at, from_mode: cur?.mode ?? null, to_mode: mode, reason, free_ram_pct: freeRamPct, cpu_pct: cpuPct, effective_cap: effectiveCap, running, writer_rev: writerRev, sample_json: sample });
1077
+ upsertRow(db, 'throttle_state', { id: 1, mode, effective_cap: effectiveCap, heavy_cap: heavyCap, running, free_ram_pct: freeRamPct, free_ram_mb: freeRamMb, cpu_pct: cpuPct, cpu_hot: int(cpuHot),
1078
+ since: changed ? at : cur.since, updated_at: at, reason, writer, writer_rev: writerRev, slot_targets_json: slotTargets, priorities_json: priorities }, ['id']);
1079
+ return { changed, from: cur?.mode ?? null };
1080
+ });
888
1081
  }
889
1082
  const throttleEvents = (m, { sinceMs = 86400000 } = {}) => m.db.prepare('SELECT * FROM throttle_events WHERE at>? ORDER BY seq').all(m.now() - sinceMs);
890
- const recordThrottleDecision = (m, { ledgerId = null, workflowId = null, jobId = null, reason, waitedMs = null }) =>
891
- Number(insertRow(m.db, 'throttle_decisions', { at: m.now(), ledger_id: ledgerId, workflow_id: workflowId, job_id: jobId, reason, waited_ms: waitedMs }).lastInsertRowid);
1083
+ const recordThrottleDecision = (m, { ledgerId = null, workflowId = null, jobId = null, reason, waitedMs = null }) => Number(insertRow(m.db, 'throttle_decisions', { at: m.now(), ledger_id: ledgerId, workflow_id: workflowId, job_id: jobId, reason, waited_ms: waitedMs }).lastInsertRowid);
892
1084
  const releaseThrottleDecision = (m, seq) => m.db.prepare('UPDATE throttle_decisions SET released_at=?, waited_ms=?-at WHERE seq=? AND released_at IS NULL').run(m.now(), m.now(), seq).changes > 0;
893
1085
  const recordHostSample = (m, sample) => Number(insertRow(m.db, 'host_samples', { at: m.now(), ...snake(sample) }).lastInsertRowid);
894
1086
  const hostSamples = (m, { kind = 'host', sinceMs = 3600000 } = {}) => m.db.prepare('SELECT * FROM host_samples WHERE kind=? AND at>? ORDER BY seq').all(kind, m.now() - sinceMs);
895
1087
  function setProviderHealth(m, { provider, status, failureKind = null, strikes = 0, strikeLimit = null, circuitOpenUntil = null, reason = null, ledgerId = null, attemptId = null, detail = null }) {
896
- return m.transaction((db) => {
897
- const cur = db.prepare('SELECT status FROM provider_health WHERE provider=?').get(provider);
898
- const at = m.now();
899
- upsertRow(db, 'provider_health', { provider, status, failure_kind: failureKind, strikes, strike_limit: strikeLimit, circuit_open_until: circuitOpenUntil,
900
- recovered_at: status === 'recovered' ? at : undefined, reason, updated_at: at, detail_json: detail }, ['provider']);
901
- if (cur?.status !== status) insertRow(db, 'provider_health_events', { provider, at, from_status: cur?.status ?? null, to_status: status, failure_kind: failureKind, ledger_id: ledgerId, attempt_id: attemptId, detail_json: circuitOpenUntil == null ? detail : { ...detail, circuitOpenUntil } });
902
- return { changed: cur?.status !== status };
903
- });
1088
+ return m.transaction((db) => {
1089
+ const cur = db.prepare('SELECT status FROM provider_health WHERE provider=?').get(provider);
1090
+ const at = m.now();
1091
+ upsertRow(db, 'provider_health', { provider, status, failure_kind: failureKind, strikes, strike_limit: strikeLimit, circuit_open_until: circuitOpenUntil,
1092
+ recovered_at: status === 'recovered' ? at : undefined, reason, updated_at: at, detail_json: detail }, ['provider']);
1093
+ if (cur?.status !== status)
1094
+ insertRow(db, 'provider_health_events', { provider, at, from_status: cur?.status ?? null, to_status: status, failure_kind: failureKind, ledger_id: ledgerId, attempt_id: attemptId, detail_json: circuitOpenUntil == null ? detail : { ...detail, circuitOpenUntil } });
1095
+ return { changed: cur?.status !== status };
1096
+ });
904
1097
  }
905
1098
  const providerHealth = (m) => m.db.prepare('SELECT * FROM provider_health ORDER BY provider').all();
906
1099
  const poolBackoff = (m, pool = null) => (pool ? m.db.prepare('SELECT * FROM pool_backoff WHERE pool=?').get(pool) ?? null : m.db.prepare('SELECT * FROM pool_backoff ORDER BY pool').all());
@@ -908,65 +1101,69 @@ const setPoolBackoff = (m, { pool, untilAt = null, strikes = 0, reason = null })
908
1101
  const clearPoolBackoff = (m, pool) => m.db.prepare('DELETE FROM pool_backoff WHERE pool=?').run(pool).changes > 0;
909
1102
  const setQuota = (m, { provider, window, used = null, limitValue = null, resetAt = null, source = null }) => upsertRow(m.db, 'quotas', { provider, window, used, limit_value: limitValue, reset_at: resetAt, source, observed_at: m.now() }, ['provider', 'window']);
910
1103
  const quotas = (m) => m.db.prepare('SELECT * FROM quotas ORDER BY provider, window').all();
911
-
912
1104
  const { reserveProvider, providerReservations, providerReservationUsage, markProviderReservation, releaseProviderReservation } = providerReservationMethods({ need, parse, toJson, hex });
913
-
914
- const upsertGuardJob = (m, { jobId, ledgerId = null, workflowId = null, attemptId = null, allow, hooks = null, ttlMs = 86400000 }) =>
915
- upsertRow(m.db, 'guard_jobs', { job_id: jobId, ledger_id: ledgerId, workflow_id: workflowId, attempt_id: attemptId, allow_json: allow, hooks_json: hooks, created_at: m.now(), expires_at: m.now() + ttlMs, released_at: null }, ['job_id']);
1105
+ const upsertGuardJob = (m, { jobId, ledgerId = null, workflowId = null, attemptId = null, allow, hooks = null, ttlMs = 86400000 }) => upsertRow(m.db, 'guard_jobs', { job_id: jobId, ledger_id: ledgerId, workflow_id: workflowId, attempt_id: attemptId, allow_json: allow, hooks_json: hooks, created_at: m.now(), expires_at: m.now() + ttlMs, released_at: null }, ['job_id']);
916
1106
  const guardJob = (m, jobId) => { const r = m.db.prepare('SELECT * FROM guard_jobs WHERE job_id=?').get(jobId); return r ? { ...r, allow: parse(r.allow_json), hooks: parse(r.hooks_json) } : null; };
917
1107
  const releaseGuardJob = (m, jobId) => m.db.prepare('UPDATE guard_jobs SET released_at=? WHERE job_id=? AND released_at IS NULL').run(m.now(), jobId).changes > 0;
918
1108
  const recordGuardRefusal = (m, refusal) => Number(insertRow(m.db, 'guard_refusals', { at: m.now(), ...snake(refusal) }).lastInsertRowid);
919
1109
  /** Host leases (the machine side of a ledger's two-phase reserve): drop by token. */
920
1110
  function releaseHostLeases(m, tokens) {
921
- const list = [...new Set([tokens].flat().map((t) => (typeof t === 'string' ? t : t?.token)).filter(Boolean))];
922
- let released = 0; for (const token of list) released += m.db.prepare('DELETE FROM host_leases WHERE token=?').run(token).changes;
923
- return { ok: true, released };
1111
+ const list = [...new Set([tokens].flat().map((t) => (typeof t === 'string' ? t : t?.token)).filter(Boolean))];
1112
+ let released = 0;
1113
+ for (const token of list)
1114
+ released += m.db.prepare('DELETE FROM host_leases WHERE token=?').run(token).changes;
1115
+ return { ok: true, released };
924
1116
  }
925
1117
  const hostLeases = (m) => m.db.prepare('SELECT * FROM host_leases ORDER BY resource_key').all();
926
1118
  const setBudget = (m, { scopeKey, limitValue, window = null }) => m.db.prepare('INSERT INTO budgets(scope_key,limit_value,window,updated_at) VALUES(?,?,?,?) ON CONFLICT(scope_key) DO UPDATE SET limit_value=excluded.limit_value, window=excluded.window, updated_at=excluded.updated_at')
927
- .run(scopeKey, limitValue, window, m.now());
1119
+ .run(scopeKey, limitValue, window, m.now());
928
1120
  /** Reserve units against a budget; refuses when used+reserved+units > limit. */
929
1121
  function reserveBudget(m, { scopeKey, ledgerId, jobId, units }) {
930
- return m.transaction((db) => {
931
- const b = db.prepare('SELECT * FROM budgets WHERE scope_key=?').get(scopeKey);
932
- if (!b) return { ok: false, reason: 'no-budget' };
933
- const held = db.prepare('SELECT units FROM budget_reservations WHERE scope_key=? AND ledger_id=? AND job_id=?').get(scopeKey, ledgerId, jobId);
934
- if (held) return { ok: true, already: true };
935
- if (b.used_value + b.reserved_value + units > b.limit_value) return { ok: false, reason: 'budget-exhausted', budget: b };
936
- insertRow(db, 'budget_reservations', { scope_key: scopeKey, ledger_id: ledgerId, job_id: jobId, units, at: m.now() });
937
- db.prepare('UPDATE budgets SET reserved_value=reserved_value+?, updated_at=? WHERE scope_key=?').run(units, m.now(), scopeKey);
938
- return { ok: true };
939
- });
1122
+ return m.transaction((db) => {
1123
+ const b = db.prepare('SELECT * FROM budgets WHERE scope_key=?').get(scopeKey);
1124
+ if (!b)
1125
+ return { ok: false, reason: 'no-budget' };
1126
+ const held = db.prepare('SELECT units FROM budget_reservations WHERE scope_key=? AND ledger_id=? AND job_id=?').get(scopeKey, ledgerId, jobId);
1127
+ if (held)
1128
+ return { ok: true, already: true };
1129
+ if (b.used_value + b.reserved_value + units > b.limit_value)
1130
+ return { ok: false, reason: 'budget-exhausted', budget: b };
1131
+ insertRow(db, 'budget_reservations', { scope_key: scopeKey, ledger_id: ledgerId, job_id: jobId, units, at: m.now() });
1132
+ db.prepare('UPDATE budgets SET reserved_value=reserved_value+?, updated_at=? WHERE scope_key=?').run(units, m.now(), scopeKey);
1133
+ return { ok: true };
1134
+ });
940
1135
  }
941
1136
  /** Settle a reservation: `used` units move to used_value, the rest is returned. */
942
1137
  function settleBudget(m, { scopeKey, ledgerId, jobId, used = null }) {
943
- return m.transaction((db) => {
944
- const r = db.prepare('SELECT units FROM budget_reservations WHERE scope_key=? AND ledger_id=? AND job_id=?').get(scopeKey, ledgerId, jobId);
945
- if (!r) return false;
946
- db.prepare('DELETE FROM budget_reservations WHERE scope_key=? AND ledger_id=? AND job_id=?').run(scopeKey, ledgerId, jobId);
947
- db.prepare('UPDATE budgets SET reserved_value=max(0,reserved_value-?), used_value=used_value+?, updated_at=? WHERE scope_key=?').run(r.units, used ?? r.units, m.now(), scopeKey);
948
- return true;
949
- });
1138
+ return m.transaction((db) => {
1139
+ const r = db.prepare('SELECT units FROM budget_reservations WHERE scope_key=? AND ledger_id=? AND job_id=?').get(scopeKey, ledgerId, jobId);
1140
+ if (!r)
1141
+ return false;
1142
+ db.prepare('DELETE FROM budget_reservations WHERE scope_key=? AND ledger_id=? AND job_id=?').run(scopeKey, ledgerId, jobId);
1143
+ db.prepare('UPDATE budgets SET reserved_value=max(0,reserved_value-?), used_value=used_value+?, updated_at=? WHERE scope_key=?').run(r.units, used ?? r.units, m.now(), scopeKey);
1144
+ return true;
1145
+ });
950
1146
  }
951
1147
  const budgets = (m) => m.db.prepare('SELECT * FROM budgets ORDER BY scope_key').all();
952
-
953
1148
  // ---------------------------------------------------------------------------------------------------------------------
954
1149
  // B5. GC, lanes, land queue, land runs, pushes, worktrees, env servers, UAT slots, connectors, asks
955
1150
  // ---------------------------------------------------------------------------------------------------------------------
956
1151
  const startGcRun = (m, { trigger = 'sweep', actionId = null, collectors = null, startedAt = m.now() } = {}) => Number(insertRow(m.db, 'gc_runs', { action_id: actionId, started_at: startedAt, trigger, collectors_json: collectors }).lastInsertRowid);
957
1152
  /** Finish a GC run; the full report is a blob (G4). */
958
1153
  function finishGcRun(m, runId, { freedBytes = null, counts = null, errors = null, report = null, finishedAt = m.now() } = {}) {
959
- const reportSha = report == null ? null : putMachineBlob(m, JSON.stringify(report), { mediaType: 'application/json' });
960
- return m.db.prepare('UPDATE gc_runs SET finished_at=?, freed_bytes=?, counts_json=?, errors_json=?, report_sha=? WHERE run_id=?')
961
- .run(finishedAt, freedBytes, toJson(counts), toJson(errors), reportSha, runId).changes > 0;
1154
+ const reportSha = report == null ? null : putMachineBlob(m, JSON.stringify(report), { mediaType: 'application/json' });
1155
+ return m.db.prepare('UPDATE gc_runs SET finished_at=?, freed_bytes=?, counts_json=?, errors_json=?, report_sha=? WHERE run_id=?')
1156
+ .run(finishedAt, freedBytes, toJson(counts), toJson(errors), reportSha, runId).changes > 0;
962
1157
  }
963
1158
  /** One thing GC touched (G4, MB-14). `outcome` is the final fate; a retry sets nextTryAt past the grace window. */
964
1159
  const recordGcItem = (m, item) => Number(insertRow(m.db, 'gc_items', { at: m.now(), ...snake(item) }).lastInsertRowid);
965
1160
  const updateGcItem = (m, itemId, fields) => updateRow(m.db, 'gc_items', snake(fields), { item_id: itemId }).changes > 0;
966
1161
  const gcItems = (m, { runId = null, open = false, collector = null, limit = 500 } = {}) => m.db.prepare(`SELECT * FROM gc_items WHERE 1=1 ${runId != null ? 'AND run_id=?' : ''} ${open ? 'AND outcome IS NULL' : ''} ${collector ? 'AND collector=?' : ''} ORDER BY item_id DESC LIMIT ?`)
967
- .all(...(runId != null ? [runId] : []), ...(collector ? [collector] : []), limit);
1162
+ .all(...(runId != null ? [runId] : []), ...(collector ? [collector] : []), limit);
968
1163
  function gcMark(m, runId, entries) {
969
- return m.transaction((db) => { const st = db.prepare('INSERT OR IGNORE INTO gc_marks(run_id,sha256,source,pinned) VALUES(?,?,?,?)'); for (const e of entries) { st.run(runId, e.sha256, e.source, e.pinned ? 1 : 0); } return entries.length; });
1164
+ return m.transaction((db) => { const st = db.prepare('INSERT OR IGNORE INTO gc_marks(run_id,sha256,source,pinned) VALUES(?,?,?,?)'); for (const e of entries) {
1165
+ st.run(runId, e.sha256, e.source, e.pinned ? 1 : 0);
1166
+ } return entries.length; });
970
1167
  }
971
1168
  /** A blob whose bytes were archived (zip) before the sweep: archived_at + archive_ref on the machine's blobs row. */
972
1169
  const markMachineBlobArchived = (m, { sha256: sha, archivedAt = m.now(), archiveRef }) => m.db.prepare('UPDATE blobs SET archived_at=?, archive_ref=? WHERE sha256=?').run(archivedAt, archiveRef, sha).changes > 0;
@@ -976,60 +1173,69 @@ const markMachineBlobArchived = (m, { sha256: sha, archivedAt = m.now(), archive
976
1173
  * except each seat's newest one. Returns the rows deleted.
977
1174
  */
978
1175
  const pruneSeatSnapshots = (m, { now = m.now(), seatMs }) => m.transaction((db) => {
979
- const final = db.prepare(`DELETE FROM seat_transcript_snapshots WHERE EXISTS (SELECT 1 FROM agent_sessions a
1176
+ const final = db.prepare(`DELETE FROM seat_transcript_snapshots WHERE EXISTS (SELECT 1 FROM agent_sessions a
980
1177
  WHERE a.seat_id=seat_transcript_snapshots.seat_id AND a.transcript_sha IS NOT NULL AND a.ended_at IS NOT NULL
981
1178
  AND seat_transcript_snapshots.at<=a.ended_at)`).run().changes;
982
- const aged = db.prepare(`DELETE FROM seat_transcript_snapshots WHERE at<? AND snapshot_id NOT IN
1179
+ const aged = db.prepare(`DELETE FROM seat_transcript_snapshots WHERE at<? AND snapshot_id NOT IN
983
1180
  (SELECT max(snapshot_id) FROM seat_transcript_snapshots GROUP BY seat_id)`).run(now - seatMs).changes;
984
- return final + aged;
1181
+ return final + aged;
985
1182
  });
986
1183
  const gcRuns = (m, { limit = 20 } = {}) => m.db.prepare('SELECT * FROM gc_runs ORDER BY run_id DESC LIMIT ?').all(limit);
987
-
988
1184
  function upsertLane(m, { name, worktreePath, branch, baseSha = null, headSha = null, owner, supJobId = null, state = 'open' }) {
989
- const cur = m.db.prepare('SELECT created_at FROM lanes WHERE name=?').get(name);
990
- return upsertRow(m.db, 'lanes', { name, worktree_path: path.resolve(worktreePath), branch, base_sha: baseSha, head_sha: headSha, owner, sup_job_id: supJobId, state, created_at: cur?.created_at ?? m.now() }, ['name']);
1185
+ const cur = m.db.prepare('SELECT created_at FROM lanes WHERE name=?').get(name);
1186
+ return upsertRow(m.db, 'lanes', { name, worktree_path: path.resolve(worktreePath), branch, base_sha: baseSha, head_sha: headSha, owner, sup_job_id: supJobId, state, created_at: cur?.created_at ?? m.now() }, ['name']);
991
1187
  }
992
1188
  function setLaneState(m, name, state, { reportText = null, headSha = null } = {}) {
993
- const set = { state };
994
- if (state === 'landed') set.landed_at = m.now();
995
- if (state === 'removed') set.removed_at = m.now();
996
- if (headSha) set.head_sha = headSha;
997
- if (reportText) set.report_sha = textBlob(m, reportText, 'text/markdown');
998
- return updateRow(m.db, 'lanes', set, { name }).changes > 0;
1189
+ const set = { state };
1190
+ if (state === 'landed')
1191
+ set.landed_at = m.now();
1192
+ if (state === 'removed')
1193
+ set.removed_at = m.now();
1194
+ if (headSha)
1195
+ set.head_sha = headSha;
1196
+ if (reportText)
1197
+ set.report_sha = textBlob(m, reportText, 'text/markdown');
1198
+ return updateRow(m.db, 'lanes', set, { name }).changes > 0;
999
1199
  }
1000
1200
  const laneOf = (m, name) => m.db.prepare('SELECT * FROM lanes WHERE name=?').get(name) ?? null;
1001
1201
  const lanes = (m, { state = null } = {}) => m.db.prepare(`SELECT * FROM lanes ${state ? 'WHERE state=?' : ''} ORDER BY created_at`).all(...(state ? [state] : []));
1002
1202
  /** Enqueue a land ticket (replaces the land queue dirs). The lane row is created when absent (FK). */
1003
1203
  function enqueueLand(m, { ticketId = `land-${Date.now().toString(36)}-${hex(4)}`, lane = null, commitSha, commits = null, requestedBy = `pid:${process.pid}` }) {
1004
- return m.transaction((db) => {
1005
- if (lane && !db.prepare('SELECT 1 FROM lanes WHERE name=?').get(lane)) insertRow(db, 'lanes', { name: lane, worktree_path: '', branch: `lane/${lane}`, owner: requestedBy ?? 'unknown', state: 'open', created_at: m.now() });
1006
- insertRow(db, 'land_queue', { ticket_id: ticketId, lane, commit_sha: commitSha, commits, requested_by: requestedBy, state: 'queued', enqueued_at: m.now() });
1007
- return ticketId;
1008
- });
1204
+ return m.transaction((db) => {
1205
+ if (lane && !db.prepare('SELECT 1 FROM lanes WHERE name=?').get(lane))
1206
+ insertRow(db, 'lanes', { name: lane, worktree_path: '', branch: `lane/${lane}`, owner: requestedBy ?? 'unknown', state: 'open', created_at: m.now() });
1207
+ insertRow(db, 'land_queue', { ticket_id: ticketId, lane, commit_sha: commitSha, commits, requested_by: requestedBy, state: 'queued', enqueued_at: m.now() });
1208
+ return ticketId;
1209
+ });
1009
1210
  }
1010
1211
  /** The head of the queue enters the gate when nothing is running. Returns the ticket, or {busy: running ticket}. */
1011
1212
  /** requested_by 'pid:<n>' names the process that waits for and then runs the ticket. */
1012
1213
  function claimLandGate(m, { ticketId, alive = pidAlive }) {
1013
- return m.transaction((db) => {
1014
- // A waiter or holder whose process is gone never blocks the queue: its ticket is cancelled.
1015
- for (const t of db.prepare("SELECT ticket_id, state, requested_by, busy_holder FROM land_queue WHERE state IN ('queued','running')").all()) {
1016
- const owner = /^pid:(\d+)$/.exec(String(t.requested_by ?? ''));
1017
- if (t.ticket_id !== ticketId && owner && !alive(Number(owner[1]))) db.prepare("UPDATE land_queue SET state='cancelled', finished_at=? WHERE ticket_id=?").run(m.now(), t.ticket_id);
1018
- }
1019
- const running = db.prepare("SELECT * FROM land_queue WHERE state='running' ORDER BY started_at LIMIT 1").get();
1020
- if (running && running.ticket_id !== ticketId) { db.prepare('UPDATE land_queue SET busy_holder=? WHERE ticket_id=?').run(running.ticket_id, ticketId); return { ok: false, busy: running }; }
1021
- const head = db.prepare("SELECT ticket_id FROM land_queue WHERE state='queued' ORDER BY enqueued_at, ticket_id LIMIT 1").get();
1022
- if (!running && head && head.ticket_id !== ticketId) return { ok: false, behind: head.ticket_id };
1023
- db.prepare("UPDATE land_queue SET state='running', gate_at=COALESCE(gate_at,?), started_at=COALESCE(started_at,?) WHERE ticket_id=? AND state IN ('queued','running')").run(m.now(), m.now(), ticketId);
1024
- return { ok: true, ticket: db.prepare('SELECT * FROM land_queue WHERE ticket_id=?').get(ticketId) };
1025
- });
1214
+ return m.transaction((db) => {
1215
+ // A waiter or holder whose process is gone never blocks the queue: its ticket is cancelled.
1216
+ for (const t of db.prepare("SELECT ticket_id, state, requested_by, busy_holder FROM land_queue WHERE state IN ('queued','running')").all()) {
1217
+ const owner = /^pid:(\d+)$/.exec(String(t.requested_by ?? ''));
1218
+ if (t.ticket_id !== ticketId && owner && !alive(Number(owner[1])))
1219
+ db.prepare("UPDATE land_queue SET state='cancelled', finished_at=? WHERE ticket_id=?").run(m.now(), t.ticket_id);
1220
+ }
1221
+ const running = db.prepare("SELECT * FROM land_queue WHERE state='running' ORDER BY started_at LIMIT 1").get();
1222
+ if (running && running.ticket_id !== ticketId) {
1223
+ db.prepare('UPDATE land_queue SET busy_holder=? WHERE ticket_id=?').run(running.ticket_id, ticketId);
1224
+ return { ok: false, busy: running };
1225
+ }
1226
+ const head = db.prepare("SELECT ticket_id FROM land_queue WHERE state='queued' ORDER BY enqueued_at, ticket_id LIMIT 1").get();
1227
+ if (!running && head && head.ticket_id !== ticketId)
1228
+ return { ok: false, behind: head.ticket_id };
1229
+ db.prepare("UPDATE land_queue SET state='running', gate_at=COALESCE(gate_at,?), started_at=COALESCE(started_at,?) WHERE ticket_id=? AND state IN ('queued','running')").run(m.now(), m.now(), ticketId);
1230
+ return { ok: true, ticket: db.prepare('SELECT * FROM land_queue WHERE ticket_id=?').get(ticketId) };
1231
+ });
1026
1232
  }
1027
1233
  const finishLandTicket = (m, ticketId, state) => m.db.prepare("UPDATE land_queue SET state=?, finished_at=? WHERE ticket_id=? AND state IN ('queued','running')").run(state, m.now(), ticketId).changes > 0;
1028
1234
  const landQueue = (m, { open = true } = {}) => m.db.prepare(`SELECT * FROM land_queue ${open ? "WHERE state IN ('queued','running')" : ''} ORDER BY enqueued_at, ticket_id`).all();
1029
1235
  /** One land-gate run with full stdout/stderr blobs. */
1030
1236
  function recordLandRun(m, { ticketId = null, lane = null, spanId = newSpanId(), parentSpanId = null, commitSha, commits = null, landedSha = null, result, reason = null, pushId = null, specs = null, stdout = null, stderr = null, startedAt, finishedAt = m.now() }) {
1031
- return Number(insertRow(m.db, 'land_runs', { ticket_id: ticketId, lane, span_id: spanId, parent_span_id: parentSpanId, commit_sha: commitSha, commits_json: commits ?? (commitSha ? [commitSha] : null), landed_sha: landedSha, result, reason, push_id: pushId,
1032
- specs_json: specs, stdout_sha: textBlob(m, stdout), stderr_sha: textBlob(m, stderr), started_at: startedAt ?? m.now(), finished_at: finishedAt }).lastInsertRowid);
1237
+ return Number(insertRow(m.db, 'land_runs', { ticket_id: ticketId, lane, span_id: spanId, parent_span_id: parentSpanId, commit_sha: commitSha, commits_json: commits ?? (commitSha ? [commitSha] : null), landed_sha: landedSha, result, reason, push_id: pushId,
1238
+ specs_json: specs, stdout_sha: textBlob(m, stdout), stderr_sha: textBlob(m, stderr), started_at: startedAt ?? m.now(), finished_at: finishedAt }).lastInsertRowid);
1033
1239
  }
1034
1240
  /**
1035
1241
  * The core record of one land in ONE transaction, idempotent on spanId (a replay from the outbox or a second try never
@@ -1037,46 +1243,51 @@ function recordLandRun(m, { ticketId = null, lane = null, spanId = newSpanId(),
1037
1243
  * runId). {runId, pushId, duplicate}.
1038
1244
  */
1039
1245
  function recordLandOutcome(m, { spanId, lane = null, push = null, run, laneHead = null, log: logRow = null }) {
1040
- need(spanId && run, 'recordLandOutcome needs spanId and run');
1041
- return m.transaction((db) => {
1042
- const hit = db.prepare('SELECT run_id, push_id FROM land_runs WHERE span_id=?').get(spanId);
1043
- if (hit) return { runId: Number(hit.run_id), pushId: hit.push_id == null ? null : Number(hit.push_id), duplicate: true };
1044
- if (lane && !db.prepare('SELECT 1 FROM lanes WHERE name=?').get(lane)) insertRow(db, 'lanes', { name: lane, worktree_path: '', branch: `lane/${lane}`, owner: 'land-gate', state: 'open', created_at: m.now() });
1045
- const pushId = push ? recordPush(m, push) : null;
1046
- const runId = recordLandRun(m, { ...run, lane, spanId, pushId });
1047
- if (lane && laneHead) updateRow(db, 'lanes', { head_sha: laneHead }, { name: lane });
1048
- if (logRow) log(m, { ...logRow, data: { ...logRow.data, runId } });
1049
- return { runId, pushId, duplicate: false };
1050
- });
1246
+ need(spanId && run, 'recordLandOutcome needs spanId and run');
1247
+ return m.transaction((db) => {
1248
+ const hit = db.prepare('SELECT run_id, push_id FROM land_runs WHERE span_id=?').get(spanId);
1249
+ if (hit)
1250
+ return { runId: Number(hit.run_id), pushId: hit.push_id == null ? null : Number(hit.push_id), duplicate: true };
1251
+ if (lane && !db.prepare('SELECT 1 FROM lanes WHERE name=?').get(lane))
1252
+ insertRow(db, 'lanes', { name: lane, worktree_path: '', branch: `lane/${lane}`, owner: 'land-gate', state: 'open', created_at: m.now() });
1253
+ const pushId = push ? recordPush(m, push) : null;
1254
+ const runId = recordLandRun(m, { ...run, lane, spanId, pushId });
1255
+ if (lane && laneHead)
1256
+ updateRow(db, 'lanes', { head_sha: laneHead }, { name: lane });
1257
+ if (logRow)
1258
+ log(m, { ...logRow, data: { ...logRow.data, runId } });
1259
+ return { runId, pushId, duplicate: false };
1260
+ });
1051
1261
  }
1052
1262
  const landRuns = (m, { lane = null, limit = 50 } = {}) => m.db.prepare(`SELECT * FROM land_runs ${lane ? 'WHERE lane=?' : ''} ORDER BY run_id DESC LIMIT ?`).all(...(lane ? [lane] : []), limit);
1053
1263
  /** One push (G8, MB-03): a refusal needs a stable failure signature; logs are full blobs. */
1054
1264
  function recordPush(m, { repoRoot, branch = null, head, fromSha = null, toSha = null, result, reason = null, failureSignature = null, ms = null, actionId = null, scan = null, stdout = null, stderr = null }) {
1055
- return Number(insertRow(m.db, 'pushes', { repo_root: repoKey(repoRoot), branch, head, from_sha: fromSha, to_sha: toSha, result, reason: reason == null ? null : redactText(String(reason)), failure_signature: failureSignature, ms, action_id: actionId,
1056
- scan_json: scan, stdout_sha: textBlob(m, stdout), stderr_sha: textBlob(m, stderr), at: m.now() }).lastInsertRowid);
1265
+ return Number(insertRow(m.db, 'pushes', { repo_root: repoKey(repoRoot), branch, head, from_sha: fromSha, to_sha: toSha, result, reason: reason == null ? null : redactText(String(reason)), failure_signature: failureSignature, ms, action_id: actionId,
1266
+ scan_json: scan, stdout_sha: textBlob(m, stdout), stderr_sha: textBlob(m, stderr), at: m.now() }).lastInsertRowid);
1057
1267
  }
1058
1268
  const pushes = (m, { repoRoot = null, limit = 50 } = {}) => m.db.prepare(`SELECT * FROM pushes ${repoRoot ? 'WHERE repo_root=?' : ''} ORDER BY push_id DESC LIMIT ?`).all(...(repoRoot ? [repoKey(repoRoot)] : []), limit);
1059
1269
  const upsertWorktree = (m, wt) => upsertRow(m.db, 'worktrees', { created_at: m.now(), ...snake(wt), path: path.resolve(wt.path) }, ['path']);
1060
1270
  const removedWorktree = (m, wtPath, { error = null, archivedRef = null } = {}) => m.db.prepare('UPDATE worktrees SET removed_at=CASE WHEN ? IS NULL THEN ? ELSE removed_at END, remove_error=?, archived_ref=COALESCE(?,archived_ref) WHERE path=?')
1061
- .run(error, m.now(), error, archivedRef, path.resolve(wtPath)).changes > 0;
1271
+ .run(error, m.now(), error, archivedRef, path.resolve(wtPath)).changes > 0;
1062
1272
  /**
1063
1273
  * The worktree registry (scripts/machine/worktree-registry.mjs and the worktree api files are its callers): reserve a row for a worktree about to be created,
1064
1274
  * atomically against the per-repo cap. BEGIN IMMEDIATE: two dispatches never both take the last slot. `cap` null: no cap.
1065
1275
  * {ok, live} | {ok:false, reason:'worktree-cap', live, cap}
1066
1276
  */
1067
1277
  function reserveWorktree(m, wt, { cap = null } = {}) {
1068
- return m.transaction((db) => {
1069
- const repoRoot = repoKey(wt.repoRoot);
1070
- const target = path.resolve(wt.path);
1071
- const live = db.prepare('SELECT path FROM worktrees WHERE repo_root=? AND removed_at IS NULL').all(repoRoot).filter((r) => r.path !== target).length;
1072
- if (cap != null && live >= cap) return { ok: false, reason: 'worktree-cap', live, cap };
1073
- upsertRow(db, 'worktrees', { ...snake(wt), path: target, repo_root: repoRoot, created_at: m.now(), removed_at: null, remove_error: null }, ['path']);
1074
- return { ok: true, live: live + 1 };
1075
- });
1278
+ return m.transaction((db) => {
1279
+ const repoRoot = repoKey(wt.repoRoot);
1280
+ const target = path.resolve(wt.path);
1281
+ const live = db.prepare('SELECT path FROM worktrees WHERE repo_root=? AND removed_at IS NULL').all(repoRoot).filter((r) => r.path !== target).length;
1282
+ if (cap != null && live >= cap)
1283
+ return { ok: false, reason: 'worktree-cap', live, cap };
1284
+ upsertRow(db, 'worktrees', { ...snake(wt), path: target, repo_root: repoRoot, created_at: m.now(), removed_at: null, remove_error: null }, ['path']);
1285
+ return { ok: true, live: live + 1 };
1286
+ });
1076
1287
  }
1077
1288
  /** Live (not removed) registry rows, optionally of one repo. */
1078
1289
  const liveWorktrees = (m, { repoRoot = null } = {}) => m.db.prepare(`SELECT * FROM worktrees WHERE removed_at IS NULL ${repoRoot ? 'AND repo_root=?' : ''} ORDER BY created_at`)
1079
- .all(...(repoRoot ? [repoKey(repoRoot)] : []));
1290
+ .all(...(repoRoot ? [repoKey(repoRoot)] : []));
1080
1291
  const worktreeRow = (m, wtPath) => m.db.prepare('SELECT * FROM worktrees WHERE path=?').get(path.resolve(wtPath)) ?? null;
1081
1292
  /** Every repo the registry ever held a worktree of. */
1082
1293
  const worktreeRepos = (m) => m.db.prepare('SELECT DISTINCT repo_root FROM worktrees ORDER BY repo_root').all().map((r) => r.repo_root);
@@ -1084,13 +1295,13 @@ const worktreeRepos = (m) => m.db.prepare('SELECT DISTINCT repo_root FROM worktr
1084
1295
  /** A GC collector's resume point (machine_meta gc_cursor:<name>): where its last bounded pass stopped, or null. */
1085
1296
  const gcCursor = (m, name) => m.db.prepare('SELECT value FROM machine_meta WHERE key=?').get(`gc_cursor:${name}`)?.value ?? null;
1086
1297
  const setGcCursor = (m, name, value) => (value == null
1087
- ? m.db.prepare('DELETE FROM machine_meta WHERE key=?').run(`gc_cursor:${name}`)
1088
- : m.db.prepare('INSERT INTO machine_meta(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value').run(`gc_cursor:${name}`, String(value))).changes > 0;
1298
+ ? m.db.prepare('DELETE FROM machine_meta WHERE key=?').run(`gc_cursor:${name}`)
1299
+ : m.db.prepare('INSERT INTO machine_meta(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value').run(`gc_cursor:${name}`, String(value))).changes > 0;
1089
1300
  /** The worktree GC's stop mark (a removal changed the main checkout): {at, damage, path} or null. set(null) clears it. */
1090
1301
  const worktreeGcStop = (m) => parse(m.db.prepare("SELECT value FROM machine_meta WHERE key='worktree_gc_stopped'").get()?.value ?? null);
1091
1302
  const setWorktreeGcStop = (m, stop) => (stop
1092
- ? m.db.prepare("INSERT INTO machine_meta(key,value) VALUES('worktree_gc_stopped',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(JSON.stringify(stop))
1093
- : m.db.prepare("DELETE FROM machine_meta WHERE key='worktree_gc_stopped'").run()).changes > 0;
1303
+ ? m.db.prepare("INSERT INTO machine_meta(key,value) VALUES('worktree_gc_stopped',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(JSON.stringify(stop))
1304
+ : m.db.prepare("DELETE FROM machine_meta WHERE key='worktree_gc_stopped'").run()).changes > 0;
1094
1305
  const dropWorktree = (m, wtPath) => m.db.prepare('DELETE FROM worktrees WHERE path=? AND removed_at IS NULL').run(path.resolve(wtPath)).changes > 0;
1095
1306
  const upsertEnvServer = (m, server) => upsertRow(m.db, 'env_servers', snake(server), ['server_id']);
1096
1307
  const envServer = (m, serverId) => m.db.prepare('SELECT * FROM env_servers WHERE server_id=?').get(serverId) ?? null;
@@ -1098,11 +1309,9 @@ const envServers = (m, { live = false } = {}) => m.db.prepare(`SELECT * FROM env
1098
1309
  const upsertUatSlot = (m, slot) => upsertRow(m.db, 'uat_slots', snake(slot), ['slot_id']);
1099
1310
  const uatSlots = (m, { live = true } = {}) => m.db.prepare(`SELECT * FROM uat_slots ${live ? 'WHERE released_at IS NULL' : ''} ORDER BY acquired_at`).all();
1100
1311
  const releaseUatSlot = (m, slotId) => m.db.prepare('UPDATE uat_slots SET released_at=? WHERE slot_id=? AND released_at IS NULL').run(m.now(), slotId).changes > 0;
1101
- const upsertConnector = (m, { name, kind = null, state = null, pid = null, port = null, publicUrl = null, config = undefined, cursor = undefined }) =>
1102
- upsertRow(m.db, 'connectors', { name, kind, state, pid, port, public_url: publicUrl, config_json: config, cursor_json: cursor, updated_at: m.now() }, ['name']);
1312
+ const upsertConnector = (m, { name, kind = null, state = null, pid = null, port = null, publicUrl = null, config = undefined, cursor = undefined }) => upsertRow(m.db, 'connectors', { name, kind, state, pid, port, public_url: publicUrl, config_json: config, cursor_json: cursor, updated_at: m.now() }, ['name']);
1103
1313
  const connectorOf = (m, name) => { const r = m.db.prepare('SELECT * FROM connectors WHERE name=?').get(name); return r ? { ...r, config: parse(r.config_json), cursor: parse(r.cursor_json) } : null; };
1104
1314
  const upsertAsk = (m, ask) => upsertRow(m.db, 'ask_requests', { asked_at: m.now(), ...snake(ask) }, ['ask_id']);
1105
-
1106
1315
  // ---------------------------------------------------------------------------------------------------------------------
1107
1316
  // B6. Observation: machine_logs (+FTS), metrics, notifications, archives
1108
1317
  // ---------------------------------------------------------------------------------------------------------------------
@@ -1113,65 +1322,94 @@ const LOG_ACTORS = new Set(['reconciler', 'supervisor', 'worker', 'gc', 'host',
1113
1322
  * 64 KiB goes to a blob. Returns the number written.
1114
1323
  */
1115
1324
  function log(m, rows) {
1116
- const list = (Array.isArray(rows) ? rows : [rows]).filter(Boolean);
1117
- if (!list.length) return 0;
1118
- return m.transaction((db) => {
1119
- let n = 0;
1120
- for (const r of list) {
1121
- need(LOG_ACTORS.has(r.actor), `machine log: unknown actor ${r.actor}`);
1122
- const { json } = jsonOrBlob(m, r.data == null ? null : redactData(r.data));
1123
- n += insertRow(db, 'machine_logs', { at: r.at ?? m.now(), actor: r.actor, controller: r.controller ?? null, ledger_id: r.ledgerId ?? null, workflow_id: r.workflowId ?? null,
1124
- job_id: r.jobId ?? null, attempt_id: int(r.attemptId), action_id: r.actionId ?? null, trace_id: r.traceId ?? null, span_id: r.spanId ?? null, level: r.level ?? 'info',
1125
- kind: String(r.kind), msg: redactText(String(r.msg ?? '')), data_json: json, refs_json: r.refs == null ? null : redactData(r.refs), src: r.src ?? null }, { orIgnore: true }).changes;
1126
- }
1127
- return n;
1128
- });
1325
+ const list = (Array.isArray(rows) ? rows : [rows]).filter(Boolean);
1326
+ if (!list.length)
1327
+ return 0;
1328
+ return m.transaction((db) => {
1329
+ let n = 0;
1330
+ for (const r of list) {
1331
+ need(LOG_ACTORS.has(r.actor), `machine log: unknown actor ${r.actor}`);
1332
+ const { json } = jsonOrBlob(m, r.data == null ? null : redactData(r.data));
1333
+ n += insertRow(db, 'machine_logs', { at: r.at ?? m.now(), actor: r.actor, controller: r.controller ?? null, ledger_id: r.ledgerId ?? null, workflow_id: r.workflowId ?? null,
1334
+ job_id: r.jobId ?? null, attempt_id: int(r.attemptId), action_id: r.actionId ?? null, trace_id: r.traceId ?? null, span_id: r.spanId ?? null, level: r.level ?? 'info',
1335
+ kind: String(r.kind), msg: redactText(String(r.msg ?? '')), data_json: json, refs_json: r.refs == null ? null : redactData(r.refs), src: r.src ?? null }, { orIgnore: true }).changes;
1336
+ }
1337
+ return n;
1338
+ });
1129
1339
  }
1130
1340
  function logs(m, { actor = null, kind = null, level = null, ledgerId = null, workflowId = null, jobId = null, since = null, search = null, limit = 200 } = {}) {
1131
- const where = [], args = [];
1132
- if (actor) { where.push('l.actor=?'); args.push(actor); }
1133
- if (kind) { where.push(kind.endsWith('*') ? 'l.kind LIKE ?' : 'l.kind=?'); args.push(kind.endsWith('*') ? `${kind.slice(0, -1)}%` : kind); }
1134
- if (level) { where.push('l.level=?'); args.push(level); }
1135
- if (ledgerId) { where.push('l.ledger_id=?'); args.push(ledgerId); }
1136
- if (workflowId) { where.push('l.workflow_id=?'); args.push(workflowId); }
1137
- if (jobId) { where.push('l.job_id=?'); args.push(jobId); }
1138
- if (since != null) { where.push('l.seq>?'); args.push(since); }
1139
- if (search) { where.push('l.seq IN (SELECT rowid FROM machine_logs_fts WHERE machine_logs_fts MATCH ?)'); args.push(search); }
1140
- return m.db.prepare(`SELECT l.* FROM machine_logs l ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY l.seq DESC LIMIT ?`).all(...args, limit)
1141
- .map((r) => ({ ...r, data: fullJson(parse(r.data_json)), refs: parse(r.refs_json) }));
1341
+ const where = [], args = [];
1342
+ if (actor) {
1343
+ where.push('l.actor=?');
1344
+ args.push(actor);
1345
+ }
1346
+ if (kind) {
1347
+ where.push(kind.endsWith('*') ? 'l.kind LIKE ?' : 'l.kind=?');
1348
+ args.push(kind.endsWith('*') ? `${kind.slice(0, -1)}%` : kind);
1349
+ }
1350
+ if (level) {
1351
+ where.push('l.level=?');
1352
+ args.push(level);
1353
+ }
1354
+ if (ledgerId) {
1355
+ where.push('l.ledger_id=?');
1356
+ args.push(ledgerId);
1357
+ }
1358
+ if (workflowId) {
1359
+ where.push('l.workflow_id=?');
1360
+ args.push(workflowId);
1361
+ }
1362
+ if (jobId) {
1363
+ where.push('l.job_id=?');
1364
+ args.push(jobId);
1365
+ }
1366
+ if (since != null) {
1367
+ where.push('l.seq>?');
1368
+ args.push(since);
1369
+ }
1370
+ if (search) {
1371
+ where.push('l.seq IN (SELECT rowid FROM machine_logs_fts WHERE machine_logs_fts MATCH ?)');
1372
+ args.push(search);
1373
+ }
1374
+ return m.db.prepare(`SELECT l.* FROM machine_logs l ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY l.seq DESC LIMIT ?`).all(...args, limit)
1375
+ .map((r) => ({ ...r, data: fullJson(parse(r.data_json)), refs: parse(r.refs_json) }));
1142
1376
  }
1143
1377
  /** Retention (DBTREE B6): debug rows older than 14 days, the rest older than 90 days. Returns rows deleted. */
1144
1378
  function pruneLogs(m, { debugMs = 14 * 86400000, restMs = 90 * 86400000 } = {}) {
1145
- return m.transaction((db) => db.prepare("DELETE FROM machine_logs WHERE (level='debug' AND at<?) OR at<?").run(m.now() - debugMs, m.now() - restMs).changes);
1379
+ return m.transaction((db) => db.prepare("DELETE FROM machine_logs WHERE (level='debug' AND at<?) OR at<?").run(m.now() - debugMs, m.now() - restMs).changes);
1146
1380
  }
1147
1381
  function recordMetrics(m, { kind, ledgerId = null, workflowId = null, windowMs = null, subject = null, data }) {
1148
- const { json, sha } = jsonOrBlob(m, data);
1149
- return Number(insertRow(m.db, 'metrics_snapshots', { at: m.now(), kind, ledger_id: ledgerId, workflow_id: workflowId, window_ms: windowMs, subject, data_json: json, data_sha: sha }).lastInsertRowid);
1382
+ const { json, sha } = jsonOrBlob(m, data);
1383
+ return Number(insertRow(m.db, 'metrics_snapshots', { at: m.now(), kind, ledger_id: ledgerId, workflow_id: workflowId, window_ms: windowMs, subject, data_json: json, data_sha: sha }).lastInsertRowid);
1150
1384
  }
1151
1385
  const latestMetrics = (m, { kind, ledgerId = null, workflowId = null } = {}) => { const r = m.db.prepare('SELECT * FROM metrics_snapshots WHERE kind=? AND ledger_id IS ? AND workflow_id IS ? ORDER BY snap_id DESC LIMIT 1').get(kind, ledgerId, workflowId); return r ? { ...r, data: parse(r.data_json) } : null; };
1152
1386
  /** A notification, deduplicated on dedupeKey (replaces telegram-sent.json). Returns {id, duplicate}. */
1153
1387
  function recordNotification(m, { channel, kind, text, media = null, mediaType = 'image/png', sentAt = null, delivery = null, ref = null, dedupeKey = null }) {
1154
- if (dedupeKey) { const hit = m.db.prepare('SELECT notif_id FROM notifications WHERE dedupe_key=?').get(dedupeKey); if (hit) return { id: Number(hit.notif_id), duplicate: true }; }
1155
- const mediaSha = media ? putMachineBlob(m, media, { mediaType }) : null;
1156
- return { id: Number(insertRow(m.db, 'notifications', { channel, kind, text, media_sha: mediaSha, sent_at: sentAt, delivery, ref, dedupe_key: dedupeKey }).lastInsertRowid), duplicate: false };
1388
+ if (dedupeKey) {
1389
+ const hit = m.db.prepare('SELECT notif_id FROM notifications WHERE dedupe_key=?').get(dedupeKey);
1390
+ if (hit)
1391
+ return { id: Number(hit.notif_id), duplicate: true };
1392
+ }
1393
+ const mediaSha = media ? putMachineBlob(m, media, { mediaType }) : null;
1394
+ return { id: Number(insertRow(m.db, 'notifications', { channel, kind, text, media_sha: mediaSha, sent_at: sentAt, delivery, ref, dedupe_key: dedupeKey }).lastInsertRowid), duplicate: false };
1157
1395
  }
1158
1396
  const notificationSent = (m, dedupeKey) => Boolean(m.db.prepare('SELECT 1 FROM notifications WHERE dedupe_key=?').get(dedupeKey));
1159
1397
  const markNotificationSent = (m, id, { delivery = 'sent' } = {}) => m.db.prepare('UPDATE notifications SET sent_at=?, delivery=? WHERE notif_id=?').run(m.now(), delivery, id).changes > 0;
1160
- const recordArchive = (m, archive) => upsertRow(m.db, 'archives', { created_at: m.now(), ...snake(archive), archive_path: path.resolve(archive.archivePath ?? archive.archive_path) }, ['archive_path']);
1161
-
1398
+ const recordArchive = (m, archive) => upsertRow(m.db, 'archives', { created_at: m.now(), ...snake(archive), archive_path: path.resolve((archive.archivePath ?? archive.archive_path)) }, ['archive_path']);
1162
1399
  // ---------------------------------------------------------------------------------------------------------------------
1163
1400
  // Catalog projection (agents / models from modules/models/*.yaml, rewritten at engine start)
1164
1401
  // ---------------------------------------------------------------------------------------------------------------------
1165
1402
  function projectCatalog(m, { agents = [], models = [], sourceRev = runtimeRev() }) {
1166
- return m.transaction((db) => {
1167
- const at = m.now();
1168
- for (const a of agents) upsertRow(db, 'agents', { agent: a.agent, provider: a.provider ?? null, spawn_card: a.spawnCard ?? null, cli_name: a.cliName ?? null, source_rev: sourceRev, loaded_at: at }, ['agent']);
1169
- for (const p of models) upsertRow(db, 'models', { profile: p.profile, agent: p.agent ?? null, model: p.model ?? null, pool: p.pool ?? null, max_parallel: int(p.maxParallel),
1170
- roles_json: p.roles ?? null, cost_json: p.cost ?? null, source_rev: sourceRev, loaded_at: at }, ['profile']);
1171
- return { agents: agents.length, models: models.length };
1172
- });
1403
+ return m.transaction((db) => {
1404
+ const at = m.now();
1405
+ for (const a of agents)
1406
+ upsertRow(db, 'agents', { agent: a.agent, provider: a.provider ?? null, spawn_card: a.spawnCard ?? null, cli_name: a.cliName ?? null, source_rev: sourceRev, loaded_at: at }, ['agent']);
1407
+ for (const p of models)
1408
+ upsertRow(db, 'models', { profile: p.profile, agent: p.agent ?? null, model: p.model ?? null, pool: p.pool ?? null, max_parallel: int(p.maxParallel),
1409
+ roles_json: p.roles ?? null, cost_json: p.cost ?? null, source_rev: sourceRev, loaded_at: at }, ['profile']);
1410
+ return { agents: agents.length, models: models.length };
1411
+ });
1173
1412
  }
1174
-
1175
1413
  /**
1176
1414
  * The ONE checkpoint of machine.sqlite: PASSIVE, on the checkpointer handle (openMachine({checkpointer:true}), the engine),
1177
1415
  * and only while the engine_leader row `name` still names `holder` at `epoch` with a live lease. A standby, a draining or a
@@ -1179,16 +1417,17 @@ function projectCatalog(m, { agents = [], models = [], sourceRev = runtimeRev()
1179
1417
  * Returns {busy, log, checkpointed} or {skipped: reason}.
1180
1418
  */
1181
1419
  function checkpoint(m, { name = 'reconciler', holder, epoch } = {}) {
1182
- need(m.checkpointer, 'machine-db: only the checkpointer handle (the reconciler engine leader) checkpoints machine.sqlite', 'STARCI_MACHINE_NOT_CHECKPOINTER');
1183
- need(holder && Number.isInteger(Number(epoch)), 'machine-db: checkpoint needs the leader {name, holder, epoch}');
1184
- const row = m.db.prepare('SELECT holder, epoch, expires_at FROM engine_leader WHERE name=?').get(name);
1185
- if (!row || row.holder !== holder || Number(row.epoch) !== Number(epoch)) return { skipped: `not the ${name} leader at epoch ${epoch}` };
1186
- if (Number(row.expires_at) <= m.now()) return { skipped: `the ${name} lease of epoch ${epoch} expired` };
1187
- return m.db.prepare('PRAGMA wal_checkpoint(PASSIVE)').get();
1420
+ need(m.checkpointer, 'machine-db: only the checkpointer handle (the reconciler engine leader) checkpoints machine.sqlite', 'STARCI_MACHINE_NOT_CHECKPOINTER');
1421
+ need(holder && Number.isInteger(Number(epoch)), 'machine-db: checkpoint needs the leader {name, holder, epoch}');
1422
+ const row = m.db.prepare('SELECT holder, epoch, expires_at FROM engine_leader WHERE name=?').get(name);
1423
+ if (!row || row.holder !== holder || Number(row.epoch) !== Number(epoch))
1424
+ return { skipped: `not the ${name} leader at epoch ${epoch}` };
1425
+ if (Number(row.expires_at) <= m.now())
1426
+ return { skipped: `the ${name} lease of epoch ${epoch} expired` };
1427
+ return m.db.prepare('PRAGMA wal_checkpoint(PASSIVE)').get();
1188
1428
  }
1189
1429
  /** machine_meta as an object. */
1190
1430
  const meta = (m) => Object.fromEntries(m.db.prepare('SELECT key, value FROM machine_meta').all().map((r) => [r.key, r.value]));
1191
-
1192
1431
  // ---------------------------------------------------------------------------------------------------------------------
1193
1432
  // Outbox: a typed write the store refused waits here until the next flush (the land gate flushes it).
1194
1433
  // Why a file: when machine.sqlite itself refuses the write (a persistent SQLITE_CORRUPT, a lock held past busy_timeout)
@@ -1197,120 +1436,187 @@ const meta = (m) => Object.fromEntries(m.db.prepare('SELECT key, value FROM mach
1197
1436
  // in the normal case; flushOutbox renames it before applying, so appends during a flush start a fresh file.
1198
1437
  // ---------------------------------------------------------------------------------------------------------------------
1199
1438
  const outboxFileFor = (machineFile) => `${path.resolve(machineFile)}.outbox.jsonl`;
1200
- /** The writes that may wait in the outbox: each is idempotent (recordLandOutcome on spanId, log on src). */
1439
+ // The outbox replays the JSON args a typed writer was called with, so the replay is typed by the writer's own contract.
1201
1440
  const DEFERRABLE = Object.freeze({ recordLandOutcome, log });
1202
1441
  // The outbox 'log' op stamps src:'outbox:<id>' on each row of its first arg; other ops keep args as passed.
1203
- const outboxArgs = (op, args, id) => { if (op !== 'log') return args;
1204
- return args.map((a, i) => { if (i !== 0) { return a; } return (Array.isArray(a) ? a : [a]).map((r) => ({ ...r, src: r.src ?? `outbox:${id}` })); }); };
1442
+ const outboxArgs = (op, args, id) => {
1443
+ if (op !== 'log')
1444
+ return args;
1445
+ return args.map((a, i) => { if (i !== 0) {
1446
+ return a;
1447
+ } return (Array.isArray(a) ? a : [a]).map((r) => ({ ...r, src: r.src ?? `outbox:${id}` })); });
1448
+ };
1205
1449
  /** Append one deferred write; returns its id. */
1206
1450
  function deferWrite({ op, args = [], file = null, env = process.env, error = null }) {
1207
- need(Object.hasOwn(DEFERRABLE, op), `machine-db: ${op} is not a deferrable write (${Object.keys(DEFERRABLE).join(', ')})`);
1208
- const id = `ob-${Date.now().toString(36)}-${hex(4)}`;
1209
- const list = outboxArgs(op, args, id);
1210
- const target = outboxFileFor(file ?? machineFileFor(env));
1211
- fs.mkdirSync(path.dirname(target), { recursive: true });
1212
- fs.appendFileSync(target, `${JSON.stringify({ id, at: Date.now(), pid: process.pid, op, args: list, error: error ? errText(error) : null })}\n`);
1213
- process.stderr.write(`[machine-db] deferred ${op} ${id} to ${target}` + (error ? `: ${errText(error)}` : '') + '\n');
1214
- return id;
1451
+ need(Object.hasOwn(DEFERRABLE, op), `machine-db: ${op} is not a deferrable write (${Object.keys(DEFERRABLE).join(', ')})`);
1452
+ const id = `ob-${Date.now().toString(36)}-${hex(4)}`;
1453
+ const list = outboxArgs(op, args, id);
1454
+ const target = outboxFileFor(file ?? machineFileFor(env));
1455
+ fs.mkdirSync(path.dirname(target), { recursive: true });
1456
+ fs.appendFileSync(target, `${JSON.stringify({ id, at: Date.now(), pid: process.pid, op, args: list, error: error ? errText(error) : null })}\n`);
1457
+ process.stderr.write(`[machine-db] deferred ${op} ${id} to ${target}` + (error ? `: ${errText(error)}` : '') + '\n');
1458
+ return id;
1215
1459
  }
1216
1460
  /** withMachine(API[op](m, ...args)); when the store refuses it, the write goes to the outbox. {ok, value} | {ok:false, deferred, error}. */
1217
1461
  export function writeOrDefer(op, args = [], { env = process.env, file = null } = {}) {
1218
- need(Object.hasOwn(DEFERRABLE, op), `machine-db: ${op} is not a deferrable write`);
1219
- try { return { ok: true, value: withMachine((m) => DEFERRABLE[op](m, ...args), { env, file }) }; }
1220
- catch (error) { return { ok: false, deferred: deferWrite({ op, args, file, env, error }), error: errText(error) }; }
1462
+ need(Object.hasOwn(DEFERRABLE, op), `machine-db: ${op} is not a deferrable write`);
1463
+ const write = DEFERRABLE[op]; // the hasOwn check above
1464
+ try {
1465
+ return { ok: true, value: withMachine((m) => write(m, ...args), { env, file }) };
1466
+ }
1467
+ catch (error) {
1468
+ return { ok: false, deferred: deferWrite({ op, args, file, env, error }), error: errText(error) };
1469
+ }
1221
1470
  }
1222
1471
  // Flush one claimed outbox file: each line through its typed writer, failed lines appended back to `base`.
1223
- const flushOutboxFile = (m, f, base) => { const back = []; let flushed = 0, failed = 0;
1224
- for (const line of fs.readFileSync(f, 'utf8').split(/\r?\n/).filter(Boolean)) {
1225
- let item = null; try { item = JSON.parse(line); } catch { back.push(line); failed += 1; continue; }
1226
- try { need(Object.hasOwn(DEFERRABLE, item.op), `unknown op ${item.op}`); m.transaction(() => DEFERRABLE[item.op](m, ...(item.args ?? []))); flushed += 1; }
1227
- catch (error) { back.push(JSON.stringify({ ...item, error: errText(error), tries: (item.tries ?? 1) + 1 })); failed += 1; } }
1228
- if (back.length) { fs.appendFileSync(base, `${back.join('\n')}\n`); }
1229
- fs.rmSync(f, { force: true });
1230
- return { flushed, failed }; };
1472
+ const flushOutboxFile = (m, f, base) => {
1473
+ const back = [];
1474
+ let flushed = 0, failed = 0;
1475
+ for (const line of fs.readFileSync(f, 'utf8').split(/\r?\n/).filter(Boolean)) {
1476
+ let item;
1477
+ try {
1478
+ item = JSON.parse(line);
1479
+ }
1480
+ catch {
1481
+ back.push(line);
1482
+ failed += 1;
1483
+ continue;
1484
+ }
1485
+ try {
1486
+ need(Object.hasOwn(DEFERRABLE, item.op), `unknown op ${item.op}`);
1487
+ m.transaction(() => DEFERRABLE[item.op](m, ...(item.args ?? [])));
1488
+ flushed += 1;
1489
+ }
1490
+ catch (error) {
1491
+ back.push(JSON.stringify({ ...item, error: errText(error), tries: (item.tries ?? 1) + 1 }));
1492
+ failed += 1;
1493
+ }
1494
+ }
1495
+ if (back.length) {
1496
+ fs.appendFileSync(base, `${back.join('\n')}\n`);
1497
+ }
1498
+ fs.rmSync(f, { force: true });
1499
+ return { flushed, failed };
1500
+ };
1231
1501
  // The outbox files this flusher owns: the live file renamed aside, plus dead flushers' leftovers.
1232
- const claimOutboxFiles = (m) => { const base = outboxFileFor(m.file), dir = path.dirname(base), stem = path.basename(base); const claimed = [];
1233
- try { const to = `${base}.${process.pid}.${Date.now()}.flushing`; fs.renameSync(base, to); claimed.push(to); }
1234
- catch (error) { if (error.code !== 'ENOENT') return { claimed, busy: errText(error), base }; }
1235
- let names = []; try { names = fs.readdirSync(dir); } catch { /* none */ }
1236
- for (const n of names) { const hit = n.startsWith(`${stem}.`) && /^(\d+)\.\d+\.flushing$/.exec(n.slice(stem.length + 1)); const full = path.join(dir, n);
1237
- if (hit && !claimed.includes(full) && Number(hit[1]) !== process.pid && !pidAlive(Number(hit[1]))) claimed.push(full); }
1238
- return { claimed, busy: null, base }; };
1502
+ const claimOutboxFiles = (m) => {
1503
+ const base = outboxFileFor(m.file), dir = path.dirname(base), stem = path.basename(base);
1504
+ const claimed = [];
1505
+ try {
1506
+ const to = `${base}.${process.pid}.${Date.now()}.flushing`;
1507
+ fs.renameSync(base, to);
1508
+ claimed.push(to);
1509
+ }
1510
+ catch (error) {
1511
+ if (error.code !== 'ENOENT')
1512
+ return { claimed, busy: errText(error), base };
1513
+ }
1514
+ let names = [];
1515
+ try {
1516
+ names = fs.readdirSync(dir);
1517
+ }
1518
+ catch { /* none */ }
1519
+ for (const n of names) {
1520
+ const hit = n.startsWith(`${stem}.`) && /^(\d+)\.\d+\.flushing$/.exec(n.slice(stem.length + 1));
1521
+ const full = path.join(dir, n);
1522
+ if (hit && !claimed.includes(full) && Number(hit[1]) !== process.pid && !pidAlive(Number(hit[1])))
1523
+ claimed.push(full);
1524
+ }
1525
+ return { claimed, busy: null, base };
1526
+ };
1239
1527
  /**
1240
1528
  * Apply every outbox line through its typed writer (each in its own transaction). A line that fails again goes back to
1241
1529
  * the outbox; a claimed file of a dead flusher is taken over. {flushed, failed, pending}.
1242
1530
  */
1243
1531
  function flushOutbox(m) {
1244
- need(!m.readOnly, 'machine-db: flushOutbox needs a writer');
1245
- const { claimed, busy, base } = claimOutboxFiles(m);
1246
- if (busy) return { flushed: 0, failed: 0, pending: base, busy };
1247
- let flushed = 0, failed = 0;
1248
- for (const f of claimed) { const r = flushOutboxFile(m, f, base); flushed += r.flushed; failed += r.failed; }
1249
- if (flushed) log(m, { actor: 'harness', kind: 'machine-db.outbox-flushed', level: failed ? 'warn' : 'info', msg: `outbox: ${flushed} deferred write(s) applied` + (failed ? `, ${failed} still pending` : ''), data: { flushed, failed } });
1250
- return { flushed, failed, pending: failed ? base : null };
1251
- }
1252
-
1532
+ need(!m.readOnly, 'machine-db: flushOutbox needs a writer');
1533
+ const { claimed, busy, base } = claimOutboxFiles(m);
1534
+ if (busy)
1535
+ return { flushed: 0, failed: 0, pending: base, busy };
1536
+ let flushed = 0, failed = 0;
1537
+ for (const f of claimed) {
1538
+ const r = flushOutboxFile(m, f, base);
1539
+ flushed += r.flushed;
1540
+ failed += r.failed;
1541
+ }
1542
+ if (flushed)
1543
+ log(m, { actor: 'harness', kind: 'machine-db.outbox-flushed', level: failed ? 'warn' : 'info', msg: `outbox: ${flushed} deferred write(s) applied` + (failed ? `, ${failed} still pending` : ''), data: { flushed, failed } });
1544
+ return { flushed, failed, pending: failed ? base : null };
1545
+ }
1253
1546
  // camelCase → snake_case keys for the pass-through writers (seat, terminal, worktree ...).
1254
1547
  function snake(obj) {
1255
- return Object.fromEntries(Object.entries(obj ?? {}).filter(([, v]) => v !== undefined).map(([k, v]) => [k.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`), v]));
1548
+ return Object.fromEntries(Object.entries(obj ?? {}).filter(([, v]) => v !== undefined).map(([k, v]) => [k.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`), v]));
1256
1549
  }
1257
-
1258
1550
  const API = {
1259
- insert: (m, table, row, opts) => insertRow(m.db, table, row, opts), upsert: (m, table, row, keys) => upsertRow(m.db, table, row, keys), update: (m, table, set, where) => updateRow(m.db, table, set, where),
1260
- putMachineBlob, jsonOrBlob, meta, checkpoint,
1261
- registerLedger, resolveLedger, listLedgers, touchLedger, setLedgerState, upsertRepository, forEachLedger, attachLedgers,
1262
- supEvent, supEvents, newestSupEvent, upsertSupJob, setSupJobStatus, supJob, listSupJobs, acquireSupLeases, releaseSupLeases, supLeases,
1263
- startSupAttempt, updateSupAttempt, latestSupAttempt, recordSupReport, supReports, consumeSupReport,
1264
- openSupDecision, setSupDecision, markSupDecisionDelivered, listSupDecisions, openOwed, ackOwed, closeOwed, listOwed,
1265
- upsertLearning, listLearning, recordOwnerRuling, upsertBridge, recordSupMessage, supMessages, markSupMessagesRead,
1266
- setSupSignal, supSignal, clearSupSignal, recordMachineLlmUsage,
1267
- startProcessRun, heartbeatProcessRun, endProcessRun, openProcessRuns, processRuns,
1268
- leaderOf, acquireLeader, renewLeader, releaseLeader, leaderHistory, cursorOf, setCursor, cursors,
1269
- enqueue, dueQueue, queueRows, dequeue, requeue, ensureSchedule, claimSchedule, finishSchedule, schedules,
1270
- actionIntent, actionRunning, actionFinish, markStaleActionsUnknown, actionOf, actions, actionStep,
1271
- controllerModes, setControllerMode, modeChanges, openSlaEpisode, markSlaViolated, markSlaReported, clearSla, openSla, recordViolation, clearViolation,
1272
- setService, recordProbe, services, serviceEvents, upsertSeat, seatOf, seats, recordDelivery, recordSeatInput, startSeatTurn, endSeatTurn, seatTranscriptSnapshot,
1273
- upsertTerminal, closeTerminal, acquireHostLock, renewHostLock, releaseHostLock, releaseObservedProvisionLock, renewProvisionLock, releaseProvisionLock, hostLock, hostLocks,
1274
- claimResource, releaseClaim, sweptClaim, liveClaims, upsertAgentSession,
1275
- throttleState, setThrottle, throttleEvents, recordThrottleDecision, releaseThrottleDecision, recordHostSample, hostSamples,
1276
- setProviderHealth, providerHealth, poolBackoff, setPoolBackoff, clearPoolBackoff, setQuota, quotas,
1277
- reserveProvider, providerReservations, providerReservationUsage, markProviderReservation, releaseProviderReservation,
1278
- upsertGuardJob, guardJob, releaseGuardJob, recordGuardRefusal, releaseHostLeases, release: releaseHostLeases, hostLeases, setBudget, reserveBudget, settleBudget, budgets,
1279
- startGcRun, finishGcRun, recordGcItem, addGcItem: recordGcItem, updateGcItem, gcItems, gcMark, addGcMarks: gcMark, gcRuns, markMachineBlobArchived, pruneSeatSnapshots,
1280
- upsertLane, setLaneState, laneOf, lanes, enqueueLand, claimLandGate, finishLandTicket, landQueue, recordLandRun, recordLandOutcome, landRuns, recordPush, pushes, flushOutbox,
1281
- upsertWorktree, removedWorktree, reserveWorktree, liveWorktrees, worktreeRow, worktreeRepos, dropWorktree, worktreeGcStop, setWorktreeGcStop, gcCursor, setGcCursor, upsertEnvServer, envServer, envServers, upsertUatSlot, uatSlots, releaseUatSlot, upsertConnector, connectorOf, upsertAsk,
1282
- log, logs, pruneLogs, recordMetrics, latestMetrics, recordNotification, notificationSent, markNotificationSent, recordArchive, projectCatalog,
1551
+ insert: (m, table, row, opts) => insertRow(m.db, table, row, opts), upsert: (m, table, row, keys) => upsertRow(m.db, table, row, keys),
1552
+ update: (m, table, set, where) => updateRow(m.db, table, set, where),
1553
+ putMachineBlob, jsonOrBlob, meta, checkpoint,
1554
+ registerLedger, resolveLedger, listLedgers, touchLedger, setLedgerState, upsertRepository, forEachLedger, attachLedgers,
1555
+ supEvent, supEvents, newestSupEvent, upsertSupJob, setSupJobStatus, supJob, listSupJobs, acquireSupLeases, releaseSupLeases, supLeases,
1556
+ startSupAttempt, updateSupAttempt, latestSupAttempt, recordSupReport, supReports, consumeSupReport,
1557
+ openSupDecision, setSupDecision, markSupDecisionDelivered, listSupDecisions, openOwed, ackOwed, closeOwed, listOwed,
1558
+ upsertLearning, listLearning, recordOwnerRuling, upsertBridge, recordSupMessage, supMessages, markSupMessagesRead,
1559
+ setSupSignal, supSignal, clearSupSignal, recordMachineLlmUsage,
1560
+ startProcessRun, heartbeatProcessRun, endProcessRun, openProcessRuns, processRuns,
1561
+ leaderOf, acquireLeader, renewLeader, releaseLeader, leaderHistory, cursorOf, setCursor, cursors,
1562
+ enqueue, dueQueue, queueRows, dequeue, requeue, ensureSchedule, claimSchedule, finishSchedule, schedules,
1563
+ actionIntent, actionRunning, actionFinish, markStaleActionsUnknown, actionOf, actions, actionStep,
1564
+ controllerModes, setControllerMode, modeChanges, openSlaEpisode, markSlaViolated, markSlaReported, clearSla, openSla, recordViolation, clearViolation,
1565
+ setService, recordProbe, services, serviceEvents, upsertSeat, seatOf, seats, recordDelivery, recordSeatInput, startSeatTurn, endSeatTurn, seatTranscriptSnapshot,
1566
+ upsertTerminal, closeTerminal, acquireHostLock, renewHostLock, releaseHostLock, releaseObservedProvisionLock, renewProvisionLock, releaseProvisionLock, hostLock, hostLocks,
1567
+ claimResource, releaseClaim, sweptClaim, liveClaims, upsertAgentSession,
1568
+ throttleState, setThrottle, throttleEvents, recordThrottleDecision, releaseThrottleDecision, recordHostSample, hostSamples,
1569
+ setProviderHealth, providerHealth, poolBackoff, setPoolBackoff, clearPoolBackoff, setQuota, quotas,
1570
+ reserveProvider, providerReservations, providerReservationUsage, markProviderReservation, releaseProviderReservation,
1571
+ upsertGuardJob, guardJob, releaseGuardJob, recordGuardRefusal, releaseHostLeases, release: releaseHostLeases, hostLeases, setBudget, reserveBudget, settleBudget, budgets,
1572
+ startGcRun, finishGcRun, recordGcItem, addGcItem: recordGcItem, updateGcItem, gcItems, gcMark, addGcMarks: gcMark, gcRuns, markMachineBlobArchived, pruneSeatSnapshots,
1573
+ upsertLane, setLaneState, laneOf, lanes, enqueueLand, claimLandGate, finishLandTicket, landQueue, recordLandRun, recordLandOutcome, landRuns, recordPush, pushes, flushOutbox,
1574
+ upsertWorktree, removedWorktree, reserveWorktree, liveWorktrees, worktreeRow, worktreeRepos, dropWorktree, worktreeGcStop, setWorktreeGcStop, gcCursor, setGcCursor, upsertEnvServer, envServer, envServers, upsertUatSlot, uatSlots, releaseUatSlot, upsertConnector, connectorOf, upsertAsk,
1575
+ log, logs, pruneLogs, recordMetrics, latestMetrics, recordNotification, notificationSent, markNotificationSent, recordArchive, projectCatalog,
1283
1576
  };
1284
-
1285
1577
  /** Every typed function at module level too: fn(handle, ...args) — blob-gc and callers holding a handle. */
1286
1578
  export { putMachineBlob, meta, checkpoint, registerLedger, resolveLedger, listLedgers, setLedgerState, upsertRepository, forEachLedger, attachLedgers, supEvent, supEvents, newestSupEvent, upsertSupJob, setSupJobStatus, supJob, listSupJobs, acquireSupLeases, releaseSupLeases, startSupAttempt, updateSupAttempt, latestSupAttempt, recordSupReport, supReports, consumeSupReport, openSupDecision, setSupDecision, markSupDecisionDelivered, listSupDecisions, ackOwed, upsertLearning, recordSupMessage, supMessages, setSupSignal, supSignal, recordMachineLlmUsage, startProcessRun, heartbeatProcessRun, endProcessRun, openProcessRuns, leaderOf, acquireLeader, renewLeader, releaseLeader, cursorOf, setCursor, cursors, enqueue, requeue, ensureSchedule, claimSchedule, finishSchedule, schedules, actionIntent, actionRunning, actionFinish, actionOf, actions, controllerModes, setControllerMode, openSlaEpisode, markSlaViolated, markSlaReported, clearSla, recordViolation, setService, recordProbe, services, upsertSeat, seatOf, seats, recordDelivery, recordSeatInput, seatTranscriptSnapshot, upsertTerminal, closeTerminal, acquireHostLock, renewHostLock, releaseHostLock, hostLock, claimResource, releaseClaim, throttleState, setThrottle, recordThrottleDecision, releaseThrottleDecision, recordHostSample, setProviderHealth, providerHealth, poolBackoff, setPoolBackoff, clearPoolBackoff, setQuota, quotas, setBudget, budgets, startGcRun, finishGcRun, recordGcItem, gcRuns, upsertLane, laneOf, lanes, enqueueLand, claimLandGate, finishLandTicket, landQueue, recordLandRun, recordLandOutcome, landRuns, recordPush, pushes, flushOutbox, removedWorktree, reserveWorktree, liveWorktrees, worktreeRow, worktreeRepos, dropWorktree, worktreeGcStop, setWorktreeGcStop, gcCursor, setGcCursor, upsertEnvServer, envServer, envServers, upsertUatSlot, releaseUatSlot, connectorOf, upsertAsk, log, logs, recordMetrics };
1287
1579
  export { reserveProvider, providerReservations, providerReservationUsage, markProviderReservation, releaseProviderReservation };
1288
-
1289
1580
  /** Best-effort machine log line from anywhere (never throws): opens, appends, closes. */
1290
1581
  export function machineLog(row, { env = process.env } = {}) {
1291
- try { return withMachine((m) => m.log(row), { env }); } catch { return 0; }
1582
+ try {
1583
+ return withMachine((m) => m.log(row), { env });
1584
+ }
1585
+ catch {
1586
+ return 0;
1587
+ }
1292
1588
  }
1293
-
1294
1589
  // ---------------------------------------------------------------------------------------------------------------------
1295
1590
  // CLI: starci runtime machine-db <init|status|ledgers|register|resolve> [--file <machine.sqlite>] [--json]
1296
1591
  // ---------------------------------------------------------------------------------------------------------------------
1297
1592
  if (isMain(import.meta.url)) {
1298
- const args = process.argv.slice(2);
1299
- const flag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
1300
- const cmd = args[0] ?? 'status';
1301
- const file = flag('file');
1302
- const out = (value) => process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
1303
- try {
1304
- if (cmd === 'init' || cmd === 'status') {
1305
- withMachine((m) => out({ ok: true, file: m.file, meta: m.meta(), userVersion: Number(pragma(m.db, 'user_version')), journalMode: pragma(m.db, 'journal_mode'),
1306
- modes: m.controllerModes(), ledgers: m.listLedgers().length, integrity: m.db.prepare('PRAGMA quick_check').get()?.quick_check,
1307
- foreignKeyViolations: m.db.prepare('PRAGMA foreign_key_check').all().length }), { file });
1308
- } else if (cmd === 'ledgers') {
1309
- withMachine((m) => out(m.listLedgers({ includeRetired: args.includes('--all') })), { file });
1310
- } else if (cmd === 'register') {
1311
- withMachine((m) => out(m.registerLedger({ ledgerId: flag('ledger-id'), name: flag('name'), repoRoot: flag('repo'), file: flag('ledger-file'), product: flag('product') })), { file });
1312
- } else if (cmd === 'resolve') {
1313
- withMachine((m) => out(m.resolveLedger({ ledgerId: flag('ledger-id'), name: flag('name'), repoRoot: flag('repo'), create: args.includes('--create') })), { file });
1314
- } else { throw new Error(`unknown command ${cmd}: init | status | ledgers [--all] | register --ledger-id --name --repo | resolve (--repo|--name|--ledger-id) [--create]`); }
1315
- } catch (error) { process.stderr.write(`${error.code ?? 'error'}: ${error.message}\n`); process.exit(2); }
1593
+ const args = process.argv.slice(2);
1594
+ const flag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] ?? null : null; };
1595
+ const cmd = args[0] ?? 'status';
1596
+ const file = flag('file');
1597
+ const out = (value) => process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
1598
+ try {
1599
+ if (cmd === 'init' || cmd === 'status') {
1600
+ withMachine((m) => out({ ok: true, file: m.file, meta: m.meta(), userVersion: Number(pragma(m.db, 'user_version')), journalMode: pragma(m.db, 'journal_mode'),
1601
+ modes: m.controllerModes(), ledgers: m.listLedgers().length, integrity: m.db.prepare('PRAGMA quick_check').get()?.quick_check,
1602
+ foreignKeyViolations: m.db.prepare('PRAGMA foreign_key_check').all().length }), { file });
1603
+ }
1604
+ else if (cmd === 'ledgers') {
1605
+ withMachine((m) => out(m.listLedgers({ includeRetired: args.includes('--all') })), { file });
1606
+ }
1607
+ else if (cmd === 'register') {
1608
+ withMachine((m) => out(m.registerLedger({ ledgerId: flag('ledger-id'), name: flag('name'), repoRoot: flag('repo'), file: flag('ledger-file'), product: flag('product') })), { file });
1609
+ }
1610
+ else if (cmd === 'resolve') {
1611
+ withMachine((m) => out(m.resolveLedger({ ledgerId: flag('ledger-id'), name: flag('name'), repoRoot: flag('repo'), create: args.includes('--create') })), { file });
1612
+ }
1613
+ else {
1614
+ throw new Error(`unknown command ${cmd}: init | status | ledgers [--all] | register --ledger-id --name --repo | resolve (--repo|--name|--ledger-id) [--create]`);
1615
+ }
1616
+ }
1617
+ catch (error) {
1618
+ const failure = error;
1619
+ process.stderr.write(`${failure.code ?? 'error'}: ${failure.message}\n`);
1620
+ process.exit(2);
1621
+ }
1316
1622
  }