@starci/hfs 4.7.32 → 4.7.34

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 (473) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +13 -6
  3. package/emit/contracts.mjs +4 -3
  4. package/emit/form-bounds-reader.ts.partial +58 -0
  5. package/emit/form-bounds.mjs +155 -0
  6. package/emit/graphql-input-bounds.mjs +97 -0
  7. package/emit/input-bounds.mjs +44 -0
  8. package/emit/operation-schemas.mjs +137 -0
  9. package/emit/operations-worker.mjs +2 -2
  10. package/emit/operations.mjs +52 -16
  11. package/emit/schema-worker.mjs +17 -4
  12. package/emit/type-schema.mjs +17 -5
  13. package/package.json +9 -2
  14. package/release/gate.mjs +18 -23
  15. package/release/index.mjs +1 -1
  16. package/release/push.mjs +4 -5
  17. package/runtime/engine/admission.mjs +331 -0
  18. package/runtime/engine/canonical-json.mjs +14 -0
  19. package/runtime/engine/db/blob.mjs +380 -0
  20. package/runtime/engine/db/event-compact.mjs +73 -0
  21. package/runtime/engine/db/ledger-open.mjs +38 -0
  22. package/runtime/engine/db/ledger-paths.mjs +87 -0
  23. package/runtime/engine/db/ledger.mjs +1068 -0
  24. package/runtime/engine/db/machine-connection.mjs +150 -0
  25. package/runtime/engine/db/machine-open.mjs +60 -0
  26. package/runtime/engine/db/machine-schema.mjs +105 -0
  27. package/runtime/engine/db/machine.mjs +1317 -0
  28. package/runtime/engine/db/provider-reservations.mjs +127 -0
  29. package/runtime/engine/db/provision-authority.mjs +76 -0
  30. package/runtime/engine/db/provision-lock.mjs +29 -0
  31. package/runtime/engine/db/provision-operations.mjs +146 -0
  32. package/runtime/engine/db/ref-value.mjs +67 -0
  33. package/runtime/engine/db/schema/machine.sql +920 -0
  34. package/runtime/engine/db/schema/runtime.sql +1113 -0
  35. package/runtime/engine/refuse.mjs +11 -0
  36. package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
  37. package/runtime/knowledge/hfs/design-truths.yaml +149 -0
  38. package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
  39. package/runtime/knowledge/hfs/rules.yaml +269 -48
  40. package/runtime/knowledge/hfs/slots.yaml +113 -53
  41. package/runtime/knowledge/patterns/be/api.yaml +46 -20
  42. package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
  43. package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
  44. package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
  45. package/runtime/knowledge/patterns/be/projections.yaml +2 -2
  46. package/runtime/knowledge/patterns/be/queues.yaml +57 -22
  47. package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
  48. package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
  49. package/runtime/knowledge/patterns/be/saga.yaml +2 -2
  50. package/runtime/knowledge/sonar-gate.yaml +1 -1
  51. package/runtime/modules/kernel/command-policy.yaml +249 -0
  52. package/runtime/modules/kernel/failure-codes.yaml +211 -52
  53. package/runtime/modules/kernel/hook-input.yaml +6 -0
  54. package/runtime/modules/kernel/protected-zone.yaml +81 -0
  55. package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
  56. package/runtime/modules/kernel/roles.yaml +405 -0
  57. package/runtime/modules/models/registry.yaml +3 -1
  58. package/runtime/modules/models/runtimes.yaml +2 -0
  59. package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
  60. package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
  61. package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
  62. package/runtime/scripts/agent/codex-app-server.mjs +79 -0
  63. package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
  64. package/runtime/scripts/agent/host-agents.mjs +80 -0
  65. package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
  66. package/runtime/scripts/agent/trust-launch.mjs +191 -0
  67. package/runtime/scripts/agent/trust.mjs +523 -0
  68. package/runtime/scripts/api/fs/rename-over.mjs +21 -0
  69. package/runtime/scripts/api/fs/temp-path.mjs +8 -0
  70. package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
  71. package/runtime/scripts/api/git/diff-names.mjs +8 -0
  72. package/runtime/scripts/api/git/head-time.mjs +10 -0
  73. package/runtime/scripts/api/git/index-lock.mjs +78 -0
  74. package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
  75. package/runtime/scripts/api/git/log.mjs +5 -0
  76. package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
  77. package/runtime/scripts/api/git/merge-tree.mjs +5 -0
  78. package/runtime/scripts/api/git/rev-list.mjs +5 -0
  79. package/runtime/scripts/api/git/rev-parse.mjs +13 -0
  80. package/runtime/scripts/api/git/status-query.mjs +5 -0
  81. package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
  82. package/runtime/scripts/api/node/spawn-node.mjs +6 -0
  83. package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
  84. package/runtime/scripts/api/npm/lib.mjs +32 -0
  85. package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
  86. package/runtime/scripts/api/npm/run-npm.mjs +6 -0
  87. package/runtime/scripts/api/npm/view.mjs +30 -0
  88. package/runtime/scripts/api/npm/whoami.mjs +8 -0
  89. package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
  90. package/runtime/scripts/api/process/kill-tree.mjs +22 -0
  91. package/runtime/scripts/api/process/lib.mjs +125 -0
  92. package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
  93. package/runtime/scripts/api/process/process-list.mjs +28 -0
  94. package/runtime/scripts/api/process/system-tool.mjs +37 -0
  95. package/runtime/scripts/gates/canon-digest.mjs +81 -0
  96. package/runtime/scripts/gates/canon-pins.mjs +16 -0
  97. package/runtime/scripts/gates/commit-proof.mjs +53 -0
  98. package/runtime/scripts/gates/gate-input.mjs +98 -0
  99. package/runtime/scripts/gates/gate.mjs +669 -0
  100. package/runtime/scripts/gates/grammar-dist.mjs +206 -0
  101. package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
  102. package/runtime/scripts/gates/test-world-run.mjs +254 -0
  103. package/runtime/scripts/gates/type-impact.mjs +277 -0
  104. package/runtime/scripts/gates/type-program.mjs +113 -0
  105. package/runtime/scripts/guards/assigned-command.mjs +12 -0
  106. package/runtime/scripts/guards/call-rights.mjs +47 -0
  107. package/runtime/scripts/guards/command-guard.mjs +468 -0
  108. package/runtime/scripts/guards/command-policy.mjs +342 -0
  109. package/runtime/scripts/guards/critic-reach.mjs +82 -0
  110. package/runtime/scripts/guards/deps-guard.mjs +150 -0
  111. package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
  112. package/runtime/scripts/guards/git-pathspec.mjs +225 -0
  113. package/runtime/scripts/guards/git-policy.mjs +355 -0
  114. package/runtime/scripts/guards/guards-root.mjs +27 -0
  115. package/runtime/scripts/guards/hook-install.mjs +343 -0
  116. package/runtime/scripts/guards/hook-io.mjs +76 -0
  117. package/runtime/scripts/guards/hook-verdict.mjs +12 -0
  118. package/runtime/scripts/guards/install-verdict.mjs +41 -0
  119. package/runtime/scripts/guards/kernel-seat.mjs +63 -0
  120. package/runtime/scripts/guards/launch-verdict.mjs +74 -0
  121. package/runtime/scripts/guards/op-starci.mjs +16 -0
  122. package/runtime/scripts/guards/op-write-scope.mjs +27 -0
  123. package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
  124. package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
  125. package/runtime/scripts/guards/protected-zone.mjs +56 -0
  126. package/runtime/scripts/guards/refusals.mjs +27 -0
  127. package/runtime/scripts/guards/release-definition.mjs +185 -0
  128. package/runtime/scripts/guards/release-push-gate.mjs +72 -0
  129. package/runtime/scripts/guards/release-record.mjs +58 -0
  130. package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
  131. package/runtime/scripts/guards/rights.mjs +268 -0
  132. package/runtime/scripts/guards/seat-tools.mjs +47 -0
  133. package/runtime/scripts/guards/shell-commands.mjs +328 -0
  134. package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
  135. package/runtime/scripts/guards/verdict-process.mjs +81 -0
  136. package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
  137. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
  138. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
  139. package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
  140. package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
  141. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
  142. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
  143. package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
  144. package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
  145. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
  146. package/runtime/scripts/hfs/check.mjs +6 -4
  147. package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
  148. package/runtime/scripts/hfs/db-restamp-verb.mjs +56 -16
  149. package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
  150. package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
  151. package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
  152. package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
  153. package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
  154. package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
  155. package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
  156. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
  157. package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
  158. package/runtime/scripts/hfs/rules/edition.mjs +56 -12
  159. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
  160. package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
  161. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
  162. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
  163. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
  164. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
  165. package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
  166. package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
  167. package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
  168. package/runtime/scripts/hfs/sealed-members.mjs +3 -3
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
  170. package/runtime/scripts/hfs/slots.mjs +2 -1
  171. package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
  172. package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
  173. package/runtime/scripts/hfs/view.mjs +2 -1
  174. package/runtime/scripts/lib/app-data.mjs +11 -0
  175. package/runtime/scripts/lib/bypass-markers.mjs +272 -75
  176. package/runtime/scripts/lib/check-format.mjs +7 -0
  177. package/runtime/scripts/lib/cli-arg.mjs +120 -0
  178. package/runtime/scripts/lib/css-rules.mjs +220 -0
  179. package/runtime/scripts/lib/dom-tree.mjs +4 -0
  180. package/runtime/scripts/lib/escape.mjs +16 -0
  181. package/runtime/scripts/lib/event-items.mjs +22 -0
  182. package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
  183. package/runtime/scripts/lib/git-dir.mjs +120 -0
  184. package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
  185. package/runtime/scripts/lib/guard-command.mjs +24 -0
  186. package/runtime/scripts/lib/hook-limits.mjs +5 -0
  187. package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
  188. package/runtime/scripts/lib/js-lex.mjs +200 -0
  189. package/runtime/scripts/lib/json-formats.mjs +0 -1
  190. package/runtime/scripts/lib/language.mjs +94 -1
  191. package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
  192. package/runtime/scripts/lib/loosening-config.mjs +361 -0
  193. package/runtime/scripts/lib/loosening-state.mjs +223 -0
  194. package/runtime/scripts/lib/menu-parts.mjs +11 -0
  195. package/runtime/scripts/lib/normalize.mjs +25 -0
  196. package/runtime/scripts/lib/op-gate.mjs +15 -0
  197. package/runtime/scripts/lib/redact.mjs +208 -0
  198. package/runtime/scripts/lib/runtime-location.mjs +21 -0
  199. package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
  200. package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
  201. package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
  202. package/runtime/scripts/lib/seat-env.mjs +43 -0
  203. package/runtime/scripts/lib/secret-patterns.mjs +58 -9
  204. package/runtime/scripts/lib/shell-source.mjs +4 -0
  205. package/runtime/scripts/lib/source-phrases.mjs +34 -0
  206. package/runtime/scripts/lib/sqlite.mjs +21 -0
  207. package/runtime/scripts/lib/swarm-stack.mjs +2 -1
  208. package/runtime/scripts/lib/tar-files.mjs +39 -0
  209. package/runtime/scripts/lib/ts-analysis.mjs +10 -0
  210. package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
  211. package/runtime/scripts/lib/worktree-record.mjs +14 -0
  212. package/runtime/scripts/machine/artifact-hold.mjs +52 -0
  213. package/runtime/scripts/machine/ask-receipts.mjs +113 -0
  214. package/runtime/scripts/machine/evidence-store.mjs +223 -0
  215. package/runtime/scripts/machine/home.mjs +216 -0
  216. package/runtime/scripts/machine/host-lock.mjs +184 -0
  217. package/runtime/scripts/machine/lock-recovery.mjs +55 -0
  218. package/runtime/scripts/machine/roles-contract.mjs +90 -0
  219. package/runtime/scripts/machine/runtime-change.mjs +20 -0
  220. package/runtime/scripts/machine/workflow-tree.mjs +122 -0
  221. package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
  222. package/runtime/scripts/reconciler/guard-row.mjs +25 -0
  223. package/runtime/scripts/work/asset-slot.mjs +284 -0
  224. package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
  225. package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
  226. package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
  227. package/runtime/scripts/work/brand/brand.mjs +892 -0
  228. package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
  229. package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
  230. package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
  231. package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
  232. package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
  233. package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
  234. package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
  235. package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
  236. package/runtime/scripts/work/draw/draw-source.mjs +412 -0
  237. package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
  238. package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
  239. package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
  240. package/runtime/scripts/work/draw-grammar.mjs +175 -0
  241. package/runtime/scripts/work/draw-render-page.mjs +188 -0
  242. package/runtime/scripts/work/draw-render.mjs +626 -0
  243. package/runtime/scripts/work/grammar-proposal.mjs +279 -0
  244. package/runtime/scripts/work/png.mjs +237 -0
  245. package/runtime/scripts/work/render-tools.mjs +64 -0
  246. package/runtime/scripts/work/work-io.mjs +119 -0
  247. package/scaffold/add-inbox-lite.mjs +2 -2
  248. package/scaffold/add-plan.mjs +40 -13
  249. package/scaffold/add-queue-lite.mjs +372 -0
  250. package/scaffold/add.mjs +5 -2
  251. package/scaffold/app.mjs +1 -2
  252. package/scaffold/edition-gate.mjs +11 -7
  253. package/scaffold/lite-exports.mjs +10 -0
  254. package/scaffold/pattern-file-slots.mjs +33 -0
  255. package/scaffold/test-world.mjs +1 -1
  256. package/scaffold/yaml-block-edit.mjs +117 -0
  257. package/src/main.mjs +5 -4
  258. package/stack/deploy.mjs +7 -3
  259. package/stack/post-deploy.mjs +48 -0
  260. package/sync/index.mjs +10 -0
  261. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
  262. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
  263. package/templates/app/codegen-graphql.mjs.partial +59 -12
  264. package/templates/app/hooks/husky/pre-commit +2 -2
  265. package/templates/app/hooks/husky/pre-push +10 -3
  266. package/templates/app/package-scripts/redis-dev.json +3 -0
  267. package/templates/app/package-scripts-lite/package.json +1 -0
  268. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
  269. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  270. package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
  271. package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
  272. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
  273. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
  275. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
  276. package/templates/app/skeleton/README.md +2 -0
  277. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
  278. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
  279. package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
  280. package/templates/app/skeleton-lite/README.md +1 -1
  281. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
  282. package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
  283. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
  284. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
  285. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
  286. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
  287. package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
  288. package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
  289. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
  290. package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  291. package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
  292. package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  293. package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  294. package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
  295. package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  296. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
  297. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
  298. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
  299. package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
  300. package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  301. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  302. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
  303. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  304. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  305. package/templates/be/inbox/platform/migration.sql.tpl +1 -0
  306. package/templates/be/inbox/service.spec.ts.tpl +4 -4
  307. package/templates/be/inbox/service.ts.tpl +3 -1
  308. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
  309. package/templates/be/inbox/webhook-door.ts.tpl +3 -2
  310. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
  311. package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
  312. package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
  313. package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
  314. package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
  315. package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
  316. package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
  317. package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
  318. package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
  319. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
  320. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
  321. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
  323. package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
  324. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
  325. package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
  326. package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  327. package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
  328. package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  329. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  330. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
  331. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
  332. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
  333. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
  334. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
  335. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
  336. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
  337. package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
  338. package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  339. package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
  340. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
  341. package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
  342. package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
  343. package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
  344. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
  345. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
  346. package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
  347. package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  348. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
  349. package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
  350. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  351. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
  352. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
  353. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
  354. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  355. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  356. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
  357. package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
  358. package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
  359. package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
  360. package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
  361. package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
  362. package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
  363. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
  364. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
  365. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
  366. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
  367. package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
  368. package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
  369. package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
  370. package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
  371. package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
  372. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
  373. package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
  374. package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
  375. package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
  376. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
  377. package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
  378. package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
  379. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
  380. package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
  381. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
  382. package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
  383. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
  384. package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
  385. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
  386. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
  387. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
  388. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
  389. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
  390. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
  391. package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
  392. package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
  393. package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
  394. package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
  395. package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
  396. package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
  397. package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
  398. package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
  399. package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
  400. package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
  401. package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
  402. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
  403. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
  404. package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
  405. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
  406. package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
  407. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
  408. package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
  409. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
  410. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
  411. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
  412. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
  413. package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
  414. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
  415. package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
  416. package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
  417. package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
  418. package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
  419. package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
  420. package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
  421. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
  422. package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
  423. package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
  424. package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
  425. package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
  426. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
  427. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
  428. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
  429. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
  430. package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
  431. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
  432. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
  433. package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
  434. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
  435. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
  436. package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
  437. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
  438. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
  439. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
  440. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
  441. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
  442. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
  443. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
  444. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  445. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
  446. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
  447. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
  448. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
  449. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  450. package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
  451. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
  452. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  453. package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
  454. package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
  455. package/upgrade/check.mjs +12 -135
  456. package/upgrade/file-state.mjs +1 -16
  457. package/upgrade/index.mjs +34 -361
  458. package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
  459. package/runtime/scripts/lib/gate-loosening.mjs +0 -292
  460. package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
  461. package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
  462. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
  463. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
  464. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
  465. package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
  466. package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
  467. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
  468. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
  469. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
  470. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
  471. package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
  472. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
  473. package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
@@ -0,0 +1,380 @@
1
+ // blob.mjs — the content-addressed blob store (operational evidence and agent output) beside the two SQLite stores:
2
+ // putBlob/getBlob/statBlob/putJson write and read it; blob bytes are immutable and never removed here.
3
+ //
4
+ // Reading agent output back out of the blob store. A Work record and the
5
+ // draw subsystem cite agent output by {artifact?, sha256}; this is the one place that turns such a citation back into
6
+ // bytes, a readable file with an extension, or a directory.
7
+ // resolveBlob(ref, {db}) ref = {artifact?, sha256?} | 'blob:<sha>' | '<sha>' → {sha256, file} | null (an artifact id
8
+ // alone resolves through the ledger's job_artifacts)
9
+ // readBlobRef(ref) the verified bytes
10
+ // blobAsFile(ref, {ext}) a read-only copy with an extension beside the store (<artifact root>-views; tools that key on the
11
+ // extension: an image decoder, a browser), cached by sha
12
+ // putBundle(dir) every file under `dir` put as a blob + a starci/blob-bundle@1 manifest {files: {rel: sha}}
13
+ // put as a blob; returns the manifest sha (a draw loop: loop.json and every round)
14
+ // bundleDir(sha) the bundle materialized as a directory under <artifact root>-views (cached by sha), or null
15
+ // Each materialized view is verified against its cited digest before reuse.
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import crypto from 'node:crypto';
19
+ import { starciLocalRoot, runtimeStateDir } from '../runtime-root.mjs';
20
+ import { isSpecRun } from '../../scripts/lib/env.mjs';
21
+ import { canonicalPath, insidePath, pathKey } from '../../scripts/lib/path-key.mjs';
22
+ import { byCodeUnit } from '../by-code-unit.mjs';
23
+ import { tempRoot } from '../temp-root.mjs';
24
+
25
+ export const ARTIFACT_ROOT_ENV = 'STARCI_ARTIFACT_ROOT';
26
+ /**
27
+ * The blob store: ARTIFACT_ROOT_ENV, else <starciLocalRoot>/artifacts (<runtime root>/.runtime/artifacts on a host). Inside a node --test
28
+ * process tree a default outside the OS temp directory is replaced by a temp one, so a bare spec never writes into the checkout.
29
+ */
30
+ export const artifactRoot = (env = process.env) => {
31
+ if (env[ARTIFACT_ROOT_ENV]) return path.resolve(env[ARTIFACT_ROOT_ENV]);
32
+ const root = path.resolve(path.join(starciLocalRoot(env), 'artifacts'));
33
+ return isSpecRun(env) && !insidePath(tempRoot({ env }), root, { key: pathKey }) ? path.join(tempRoot({ env }), 'starci-test-artifacts') : root;
34
+ };
35
+ const SHA = /^[a-f0-9]{64}$/;
36
+ const assertSha = sha => {
37
+ if (typeof sha !== 'string' || !SHA.test(sha)) throw new TypeError('blob sha must be a lowercase sha256 hex digest');
38
+ return sha;
39
+ };
40
+ // An explicit root scopes READs only; writers continue to use the default external root.
41
+ const location = (sha, root = null, suffix = '') => {
42
+ if (root !== null && (typeof root !== 'string' || !root || !path.isAbsolute(root)))
43
+ throw new TypeError('blob read root must be an absolute path');
44
+ const file = path.join(root === null ? artifactRoot() : path.resolve(root), assertSha(sha).slice(0, 2), `${sha}${suffix}`);
45
+ if (root !== null) {
46
+ regularParents(file, { strict: true, base: root });
47
+ const stat = fs.lstatSync(file, { throwIfNoEntry: false });
48
+ if (stat && (!stat.isFile() || stat.isSymbolicLink()
49
+ || fs.realpathSync.native(file) !== path.join(fs.realpathSync.native(path.dirname(file)), path.basename(file))
50
+ || !fs.readdirSync(path.dirname(file)).includes(path.basename(file))))
51
+ throw new Error(`blob read path is not a contained regular file: ${file}`);
52
+ }
53
+ return file;
54
+ };
55
+ const metadataPath = (sha, root = null) => location(sha, root, '.json');
56
+ const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
57
+
58
+ // The last matching rule of a file wins, a negation un-ignores, and .gitignore outranks .git/info/exclude (git's own precedence).
59
+ const ignoreVerdict = (file, forms) => {
60
+ let verdict = null;
61
+ try {
62
+ for (const raw of fs.readFileSync(file, 'utf8').split('\n')) {
63
+ const line = raw.trim();
64
+ if (!line || line.startsWith('#')) continue;
65
+ const negated = line.startsWith('!');
66
+ if (forms.has(negated ? line.slice(1) : line)) verdict = !negated;
67
+ }
68
+ } catch { /* absent or unreadable: no rule */ }
69
+ return verdict;
70
+ };
71
+ // The state dir relative to the checkout that actually holds it (<top>/.runtime on the source, <app>/.claude/.runtime in an app repo).
72
+ const stateIgnored = (top, stateDir) => {
73
+ const rel = path.relative(top, stateDir).split(path.sep).join('/');
74
+ if (!rel || rel.startsWith('..')) return false;
75
+ const forms = new Set([rel, `${rel}/`, `/${rel}`, `/${rel}/`]);
76
+ return (ignoreVerdict(path.join(top, '.gitignore'), forms) ?? ignoreVerdict(path.join(top, '.git', 'info', 'exclude'), forms)) === true;
77
+ };
78
+ // A writer root is outside every checkout (including worktrees whose .git is a file), or it is under this runtime's own
79
+ // .runtime directory AND that directory is listed in the checkout's .gitignore (or .git/info/exclude), so a blob store can
80
+ // never be committed. Fail closed: a root under any other path in a checkout, or a .runtime nobody ignores, refuses.
81
+ function ensureExternalRoot(root, stateDirectory = runtimeStateDir()) {
82
+ const real = canonicalPath(root), stateDir = canonicalPath(stateDirectory); // canonical on both sides: a link into a checkout cannot hide it
83
+ let cursor = real;
84
+ while (true) {
85
+ if (fs.existsSync(path.join(cursor, '.git'))) {
86
+ if (!insidePath(stateDir, real, { key: pathKey, includeSelf: true })) throw new Error(`artifact root is inside a git checkout: ${root}`);
87
+ if (!stateIgnored(cursor, stateDir)) throw new Error(`artifact root is inside a git checkout and not git-ignored: ${root}`);
88
+ return;
89
+ }
90
+ const parent = path.dirname(cursor);
91
+ if (parent === cursor) return;
92
+ cursor = parent;
93
+ }
94
+ }
95
+
96
+ function sourceBytes(bufferOrPath) {
97
+ if (Buffer.isBuffer(bufferOrPath)) return bufferOrPath;
98
+ if (bufferOrPath instanceof Uint8Array) return Buffer.from(bufferOrPath);
99
+ if (typeof bufferOrPath === 'string') return fs.readFileSync(bufferOrPath);
100
+ throw new TypeError('putBlob expects a Buffer, Uint8Array, or file path');
101
+ }
102
+
103
+ function publishOnce(destination, bytes) {
104
+ const dir = path.dirname(destination);
105
+ fs.mkdirSync(dir, { recursive: true });
106
+ const temp = path.join(dir, `.${path.basename(destination)}.${process.pid}.${crypto.randomUUID()}.tmp`);
107
+ let owned=false;
108
+ try {
109
+ const fd=fs.openSync(temp,'wx');
110
+ owned=true;
111
+ try{fs.writeFileSync(fd,bytes);fs.fsyncSync(fd);}finally{fs.closeSync(fd);}
112
+ try { fs.linkSync(temp, destination); }
113
+ catch (error) { if (error.code !== 'EEXIST') throw error; }
114
+ } finally { if(owned)fs.rmSync(temp,{force:true}); }
115
+ }
116
+
117
+ /** Store original bytes under their sha256. A concurrent or repeated put never replaces published bytes. */
118
+ export function putBlob(bufferOrPath, { mediaType = 'application/octet-stream' } = {}) {
119
+ if (typeof mediaType !== 'string' || !mediaType.trim()) throw new TypeError('mediaType must be a nonempty string');
120
+ const root = artifactRoot();
121
+ ensureExternalRoot(root);
122
+ const bytes = sourceBytes(bufferOrPath);
123
+ const sha = digest(bytes);
124
+ const destination = location(sha);
125
+ const metadata = { size: bytes.length, mediaType, createdAt: new Date().toISOString() };
126
+ // Metadata is published first so a visible blob always has a sidecar. A crash can leave an orphan sidecar;
127
+ // the next put completes it. The first published media type and creation time win.
128
+ publishOnce(metadataPath(sha), Buffer.from(JSON.stringify(metadata)));
129
+ publishOnce(destination, bytes);
130
+ const stored = fs.statSync(destination);
131
+ if (stored.size !== bytes.length) throw new Error(`blob size mismatch: ${sha}`);
132
+ // Reuse must verify bytes, including a corrupt same-size destination.
133
+ getBlob(sha);
134
+ const info = statBlob(sha);
135
+ // Flush reused files too. Directory/link persistence remains platform-specific;
136
+ // this local profile does not promise power-loss recovery of acknowledged refs.
137
+ for(const file of [destination,metadataPath(sha)]){const fd=fs.openSync(file,'r+');try{fs.fsyncSync(fd);}finally{fs.closeSync(fd);}}
138
+ return { sha, size: info.size, mediaType: info.mediaType };
139
+ }
140
+
141
+ export function blobPath(sha, { root = null } = {}) {
142
+ const file = location(sha, root);
143
+ return fs.existsSync(file) && fs.statSync(file).isFile() ? file : null;
144
+ }
145
+
146
+ export function getBlob(sha, { root = null, verifyBundle = false } = {}) {
147
+ const file = blobPath(sha, { root });
148
+ if (!file) throw Object.assign(new Error(`blob not found: ${sha}`), { code: 'ENOENT' });
149
+ const bytes = fs.readFileSync(file);
150
+ if (digest(bytes) !== sha) throw new Error(`blob hash mismatch: ${sha}`);
151
+ if (root !== null) statBlob(sha, { root });
152
+ if (verifyBundle) {
153
+ let manifest;
154
+ try { manifest = JSON.parse(bytes.toString('utf8')); } catch { /* ordinary blobs need not be JSON */ }
155
+ if (typeof manifest?.schema === 'string' && manifest.schema.startsWith('starci/blob-bundle@')
156
+ && !bundleDir(sha, { root }))
157
+ throw Object.assign(new Error(`blob bundle cannot be verified: ${sha}`), { code: 'EINVALBUNDLE' });
158
+ }
159
+ return bytes;
160
+ }
161
+
162
+ export function statBlob(sha, { root = null } = {}) {
163
+ const file = blobPath(sha, { root });
164
+ if (!file) return null;
165
+ const { size } = fs.statSync(file);
166
+ const metadata = JSON.parse(fs.readFileSync(metadataPath(sha, root), 'utf8'));
167
+ if (metadata.size !== size) throw new Error(`blob sidecar size mismatch: ${sha}`);
168
+ if(typeof metadata.mediaType!=='string'||!metadata.mediaType.trim()||!Number.isFinite(Date.parse(metadata.createdAt)))throw new Error(`blob sidecar metadata invalid: ${sha}`);
169
+ return { sha, size, mediaType: metadata.mediaType, createdAt: metadata.createdAt };
170
+ }
171
+
172
+ function canonical(value) {
173
+ if (value && typeof value === 'object') {
174
+ if (typeof value.toJSON === 'function') return canonical(value.toJSON());
175
+ if (Array.isArray(value)) return value.map(item => item === undefined ? null : canonical(item));
176
+ return Object.fromEntries(Object.keys(value).sort(byCodeUnit).filter(key => value[key] !== undefined).map(key => [key, canonical(value[key])]));
177
+ }
178
+ return value;
179
+ }
180
+
181
+ /* ------------------------------------------------------------ citations, views and bundles */
182
+
183
+ const BUNDLE_SCHEMA = 'starci/blob-bundle@1';
184
+ // Read-only views (files with an extension, materialized bundles) live beside the store, e.g. <runtime root>/.runtime/artifacts-views.
185
+ const viewRoot = (root = null) => `${root === null ? artifactRoot() : path.resolve(root)}-views`;
186
+ const slash = (p) => String(p).replaceAll('\\', '/');
187
+
188
+ /** The sha256 a citation names: {sha256}, 'blob:<sha>' or a bare sha; an {artifact} alone needs `db`. */
189
+ function shaOfRef(ref, { db = null } = {}) {
190
+ if (typeof ref === 'string') { const s = ref.trim().replace(/^blob:/, ''); return SHA.test(s) ? s : null; }
191
+ if (!ref || typeof ref !== 'object') return null;
192
+ if (typeof ref.sha256 === 'string' && SHA.test(ref.sha256)) return ref.sha256;
193
+ if (ref.artifact != null && db) return db.prepare('SELECT sha256 FROM job_artifacts WHERE artifact_id=?').get(Number(ref.artifact))?.sha256 ?? null;
194
+ return null;
195
+ }
196
+
197
+ /** {sha256, file} of a citation whose bytes are in the local store, else null. */
198
+ export function resolveBlob(ref, { db = null, root = null } = {}) {
199
+ const sha = shaOfRef(ref, { db });
200
+ const file = sha ? blobPath(sha, { root }) : null;
201
+ return file ? { sha256: sha, file } : null;
202
+ }
203
+
204
+ /** A read-only copy of the blob with `ext` (e.g. '.png'), cached beside the store; null when missing. */
205
+ export function blobAsFile(ref, { ext = '', db = null, root = null } = {}) {
206
+ const hit = resolveBlob(ref, { db, root });
207
+ if (!hit) return null;
208
+ if (typeof ext !== 'string' || (ext !== '' && !/^\.[a-z0-9]{1,16}$/i.test(ext))) throw new TypeError('blob view extension must be a simple suffix');
209
+ const bytes = getBlob(hit.sha256, { root });
210
+ const file = path.join(viewRoot(root), 'files', `${hit.sha256}${ext}`);
211
+ regularParents(file, { strict: root !== null, base: path.dirname(viewRoot(root)) });
212
+ publishOnce(file, bytes);
213
+ if (!fs.lstatSync(file).isFile() || fs.lstatSync(file).isSymbolicLink() || digest(fs.readFileSync(file)) !== hit.sha256)
214
+ throw new Error(`blob view hash mismatch: ${hit.sha256}`);
215
+ if (root !== null && (fs.realpathSync.native(file) !== path.join(fs.realpathSync.native(path.dirname(file)), path.basename(file))
216
+ || !fs.readdirSync(path.dirname(file)).includes(path.basename(file))))
217
+ throw new Error(`blob view path is linked or has a different spelling: ${file}`);
218
+ return file;
219
+ }
220
+
221
+ // The trusted base (the read root, or the directory holding the store and its views) is canonicalised once; only directories BELOW it must be regular and exactly spelled.
222
+ function regularParents(file, { strict = false, base }) {
223
+ const top = path.resolve(base), levels = []; let real = null;
224
+ for (let at = path.dirname(path.resolve(file)); at !== top; at = path.dirname(at)) {
225
+ if (!insidePath(top, at)) throw new Error(`blob path is outside its trusted root: ${file}`); else levels.unshift(at);
226
+ }
227
+ for (const cursor of levels) {
228
+ const stat = fs.lstatSync(cursor, { throwIfNoEntry: false }), name = path.basename(cursor);
229
+ if (!stat) return;
230
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`blob view parent is not a regular directory: ${cursor}`);
231
+ if (!strict) continue;
232
+ real = path.join(real ?? fs.realpathSync.native(top), name);
233
+ if (fs.realpathSync.native(cursor) !== real || !fs.readdirSync(path.dirname(cursor)).includes(name))
234
+ throw new Error(`blob read parent is linked or has a different spelling: ${cursor}`);
235
+ }
236
+ }
237
+
238
+ function bundleEntries(manifest) {
239
+ if (!manifest.files || typeof manifest.files !== 'object' || Array.isArray(manifest.files)) throw new TypeError('blob bundle files must be a mapping');
240
+ const entries = Object.entries(manifest.files);
241
+ const names = new Set();
242
+ for (const [rel, sha] of entries) {
243
+ const parts = rel.split('/');
244
+ if (!rel || rel.includes('\\') || path.posix.isAbsolute(rel) || path.win32.isAbsolute(rel)
245
+ || parts.some(part => !part || part === '.' || part === '..' || /[<>:"|?*\x00-\x1f]/.test(part) || /[. ]$/.test(part)
246
+ || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)) || parts[0] === '.complete')
247
+ throw new TypeError(`unsafe blob bundle path: ${rel}`);
248
+ assertSha(sha);
249
+ const key = rel.toLowerCase();
250
+ if (names.has(key)) throw new TypeError(`blob bundle path collision: ${rel}`);
251
+ names.add(key);
252
+ }
253
+ for (const name of names) {
254
+ const parts = name.split('/');
255
+ for (let index = 1; index < parts.length; index += 1) if (names.has(parts.slice(0, index).join('/')))
256
+ throw new TypeError(`blob bundle file is also a directory: ${name}`);
257
+ }
258
+ return entries;
259
+ }
260
+
261
+ function verifiedBundle(dir, entries, opts) {
262
+ const { strict } = opts; regularParents(path.join(dir, '.complete'), opts);
263
+ if (!fs.existsSync(dir)) return false;
264
+ const expected = new Map(entries);
265
+ const seen = new Set();
266
+ const walk = current => {
267
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
268
+ const file = path.join(current, entry.name);
269
+ const rel = slash(path.relative(dir, file));
270
+ if (entry.isSymbolicLink()) throw new Error(`blob bundle view contains a link: ${rel}`);
271
+ if (strict && fs.realpathSync.native(file) !== path.join(fs.realpathSync.native(current), entry.name))
272
+ throw new Error(`blob bundle view contains a redirected path: ${rel}`);
273
+ if (entry.isDirectory()) { walk(file); continue; }
274
+ if (!entry.isFile()) throw new Error(`blob bundle view contains a special file: ${rel}`);
275
+ if (rel === '.complete') continue;
276
+ if (!expected.has(rel) || digest(fs.readFileSync(file)) !== expected.get(rel)) throw new Error(`blob bundle view hash mismatch: ${rel}`);
277
+ seen.add(rel);
278
+ }
279
+ };
280
+ walk(dir);
281
+ return seen.size === entries.length && fs.existsSync(path.join(dir, '.complete'));
282
+ }
283
+
284
+ const filesUnder = (dir, out = []) => {
285
+ for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
286
+ const full = path.join(dir, e.name);
287
+ if (e.isDirectory()) filesUnder(full, out); else if (e.isFile()) out.push(full);
288
+ }
289
+ return out;
290
+ };
291
+
292
+ /** Put every file under `dir` and its manifest in the blob store; returns the manifest sha. */
293
+ export function putBundle(dir, { mediaTypeOf = () => 'application/octet-stream' } = {}) {
294
+ const files = {};
295
+ for (const abs of filesUnder(dir)) files[slash(path.relative(dir, abs))] = putBlob(abs, { mediaType: mediaTypeOf(abs) }).sha;
296
+ return putBlob(Buffer.from(JSON.stringify({ schema: BUNDLE_SCHEMA, files }, null, 2)), { mediaType: 'application/json' }).sha;
297
+ }
298
+
299
+ /** The manifest of a bundle: {schema, files: {rel: sha}} or null. */
300
+ function bundleManifest(ref, { db = null, root = null } = {}) {
301
+ const hit = resolveBlob(ref, { db, root });
302
+ if (!hit) return null;
303
+ const bytes = getBlob(hit.sha256, { root });
304
+ let doc;
305
+ try { doc = JSON.parse(bytes.toString('utf8')); } catch { return null; }
306
+ return doc?.schema === BUNDLE_SCHEMA ? doc : null;
307
+ }
308
+
309
+ /** Direct blob references of a bundle, validated by the storage format owner; ordinary blobs have none. Reads never materialize a view. */
310
+ export function blobReferences(ref, { root = null } = {}) {
311
+ const manifest = bundleManifest(ref, { root });
312
+ return manifest ? bundleEntries(manifest).map(([, sha]) => sha) : [];
313
+ }
314
+
315
+ /**
316
+ * Remove a staging directory this call made: the files it wrote, then the directories above them, deepest first. It never walks a tree: the link-safe
317
+ * recursive delete is scripts/api/fs/safe-remove.mjs, which the db tier cannot import, and a staging tree holds only what this call created.
318
+ */
319
+ function dropStaging(staging, written) {
320
+ const dirs = new Set([staging]);
321
+ for (const file of written) {
322
+ if (fs.existsSync(file)) fs.rmSync(file);
323
+ for (let dir = path.dirname(file); dir !== staging && !dirs.has(dir); dir = path.dirname(dir)) dirs.add(dir);
324
+ }
325
+ for (const dir of [...dirs].sort((a, b) => b.length - a.length)) if (fs.existsSync(dir)) fs.rmdirSync(dir);
326
+ }
327
+
328
+ /** Write one bundle member set into `staging` (unsafe rel paths refuse); `written` collects every path for dropStaging. */
329
+ function writeBundleStaging(staging, members, sha, written) {
330
+ for (const [rel, bytes] of members) {
331
+ const to = path.resolve(staging, ...rel.split('/'));
332
+ const relative = path.relative(staging, to);
333
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error(`unsafe blob bundle target: ${rel}`);
334
+ fs.mkdirSync(path.dirname(to), { recursive: true });
335
+ written.push(to);
336
+ fs.writeFileSync(to, bytes, { flag: 'wx' });
337
+ }
338
+ written.push(path.join(staging, '.complete'));
339
+ fs.writeFileSync(written.at(-1), sha, { flag: 'wx' });
340
+ }
341
+
342
+ /** The bundle as a directory beside the store (cached by sha), or null when it or one file is missing. */
343
+ export function bundleDir(ref, { db = null, root = null } = {}) {
344
+ const sha = shaOfRef(ref, { db });
345
+ const manifest = sha ? bundleManifest(sha, { root }) : null;
346
+ if (!manifest) return null;
347
+ const entries = bundleEntries(manifest);
348
+ const members = [];
349
+ for (const [rel, fileSha] of entries) {
350
+ try { members.push([rel, getBlob(fileSha, { root })]); }
351
+ catch (error) { if (error?.code === 'ENOENT') { return null; } throw error; }
352
+ }
353
+ const dir = path.join(viewRoot(root), 'bundles', sha), opts = { strict: root !== null, base: path.dirname(viewRoot(root)) };
354
+ if (verifiedBundle(dir, entries, opts)) return dir;
355
+ if (fs.existsSync(dir)) throw new Error(`blob bundle view is incomplete: ${sha}`);
356
+ regularParents(dir, opts);
357
+ fs.mkdirSync(path.dirname(dir), { recursive: true });
358
+ const staging = fs.mkdtempSync(path.join(path.dirname(dir), `.${sha}-`));
359
+ const written = [];
360
+ try {
361
+ writeBundleStaging(staging, members, sha, written);
362
+ try { fs.renameSync(staging, dir); }
363
+ catch (error) { if (!fs.existsSync(dir) || !verifiedBundle(dir, entries, opts)) throw error; }
364
+ } finally {
365
+ dropStaging(staging, written);
366
+ }
367
+ if (!verifiedBundle(dir, entries, opts)) throw new Error(`blob bundle publication is incomplete: ${sha}`);
368
+ return dir;
369
+ }
370
+
371
+ /**
372
+ * The readable file of one record asset: a product asset kept in the tree ({path}, a ui direction the owner accepted,
373
+ * decision Q2) resolves against the record's directory; agent output ({artifact?, sha256, name}) resolves in the blob
374
+ * store as a copy carrying the extension of its artifact name. Null when neither is readable.
375
+ */
376
+ export function assetFileOf(recordDir, asset, { db = null, root = null } = {}) {
377
+ if (!asset || typeof asset !== 'object') return null;
378
+ if (typeof asset.path === 'string' && asset.path) { const file = path.resolve(recordDir, asset.path); return fs.existsSync(file) ? file : null; }
379
+ return blobAsFile(asset, { ext: path.extname(String(asset.name ?? '')).toLowerCase(), db, root });
380
+ }
@@ -0,0 +1,73 @@
1
+ // event-compact.mjs — the bounded inline view of an event payload, and the one place the event size bound is declared.
2
+ // A launch admission decision (every candidate with its quota snapshot, the pick record) and an admission refusal grow with the number of
3
+ // candidates, so no event may carry one whole: appendEvent (ledger.mjs) stores the full payload as a blob (events.payload_sha) and keeps this
4
+ // summary inline. Readers that need the whole decision read the blob behind the row's payload_sha.
5
+ import { isPlainObject } from '../plain-object.mjs';
6
+ import { list } from '../../scripts/lib/list.mjs';
7
+ import { redactData } from '../../scripts/lib/redact.mjs';
8
+
9
+ /** Declared sizes: the ledger event bound and the inline summary caps (every cap keeps the summary far below the bound). */
10
+ export const EVENT_LIMITS = Object.freeze({ payloadBytes: 16384, rows: 8, ids: 8, stringChars: 200, lineChars: 600, depth: 8 });
11
+ const ADMISSION_SCHEMA = 'starci/agent-admission@1';
12
+ export const REFUSAL_SCHEMA = 'starci/admission-refusal@1';
13
+
14
+ const clip = (value, max = EVENT_LIMITS.stringChars) => (typeof value === 'string' && value.length > max ? `${value.slice(0, max)}...` : value);
15
+
16
+ /** The primitive fields of an object, long strings clipped: quota snapshots and other nested records are dropped. */
17
+ function scalars(value) {
18
+ if (!isPlainObject(value)) return value ?? null;
19
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v === null || ['string', 'number', 'boolean'].includes(typeof v)).map(([k, v]) => [k, clip(v)]));
20
+ }
21
+ const rowOf = (row) => ({ id: row?.id ?? null, provider: row?.provider ?? null, model: row?.model ?? null, codes: list(row?.codes).slice(0, EVENT_LIMITS.rows).map((code) => clip(code, 80)) });
22
+
23
+ function admissionSummary(a) {
24
+ const receipt = isPlainObject(a.receipt) ? { ...scalars(a.receipt), ...(isPlainObject(a.receipt.scope) ? { scope: scalars(a.receipt.scope) } : {}) } : null;
25
+ return { schema: a.schema, ok: a.ok === true, reason: clip(a.reason ?? null), policyVersion: a.policyVersion ?? null, chosenBy: clip(a.chosenBy ?? null),
26
+ overrideApplied: a.overrideApplied === true, resetAt: a.resetAt ?? null, request: scalars(a.request), selected: scalars(a.selected),
27
+ eligibleCount: list(a.eligible).length, eligible: list(a.eligible).slice(0, EVENT_LIMITS.ids).map((row) => row?.id ?? null),
28
+ rejectedCount: list(a.rejected).length, rejected: list(a.rejected).slice(0, EVENT_LIMITS.rows).map(rowOf),
29
+ ...(receipt ? { receipt } : {}), summarized: true };
30
+ }
31
+
32
+ function refusalSummary(r) {
33
+ const rows = list(r.rejected), alternatives = list(r.alternatives);
34
+ return { schema: r.schema, class: r.class ?? null, queuedBecause: r.queuedBecause ?? null, line: clip(r.line ?? null, EVENT_LIMITS.lineChars),
35
+ rejectedCount: rows.length, rejected: rows.slice(0, EVENT_LIMITS.rows).map(rowOf),
36
+ alternativesCount: alternatives.length, alternatives: alternatives.slice(0, EVENT_LIMITS.ids).map((entry) => clip(String(entry))), summarized: true };
37
+ }
38
+
39
+ const SUMMARIES = Object.freeze({ [ADMISSION_SCHEMA]: admissionSummary, [REFUSAL_SCHEMA]: refusalSummary });
40
+
41
+ /** `{payload, compacted}`: every admission-shaped record in `payload` replaced by its bounded summary; `compacted` says whether one was. */
42
+ function compactEventPayload(payload, depth = 0) {
43
+ if (depth > EVENT_LIMITS.depth || payload === null || typeof payload !== 'object') return { payload, compacted: false };
44
+ if (isPlainObject(payload) && typeof payload.schema === 'string' && payload.summarized !== true && SUMMARIES[payload.schema])
45
+ return { payload: SUMMARIES[payload.schema](payload), compacted: true };
46
+ let compacted = false;
47
+ const visit = (value) => { const inner = compactEventPayload(value, depth + 1); compacted ||= inner.compacted; return inner.payload; };
48
+ const copy = Array.isArray(payload) ? payload.map((item) => visit(item)) : Object.fromEntries(Object.entries(payload).map(([k, v]) => [k, visit(v)]));
49
+ return { payload: compacted ? copy : payload, compacted };
50
+ }
51
+
52
+ /** A payload's bounded inline view when it went whole to the blob store: its top-level scalars (long strings clipped) and where the rest is. */
53
+ function spilledView(redacted, bytes, sha) {
54
+ const flat = isPlainObject(redacted) ? scalars(redacted) : {};
55
+ return { ...flat, spilled: true, sha256: sha, bytes };
56
+ }
57
+
58
+ /**
59
+ * The stored form of an event payload: `{payloadJson, payloadSha}`. The payload is redacted. It goes whole to the blob store (`spill(bytes)`
60
+ * stores it and returns its sha, kept when the caller already holds one) when it carries an admission record (the bounded summary is the inline
61
+ * JSON) or when it is over the inline bound (the inline JSON is its top-level scalars and the sha). This is the one path of every event writer:
62
+ * a payload that grows with its input (a file list, a manifest, a critique, a menu) never fails the write, and a reader that needs the whole
63
+ * payload goes through engine/db/event-payload.mjs.
64
+ */
65
+ export function eventPayloadRecord(payload, payloadSha, spill) {
66
+ if (payload === null || payload === undefined) return { payloadJson: null, payloadSha };
67
+ const redacted = redactData(payload);
68
+ const { payload: inline, compacted } = compactEventPayload(redacted);
69
+ const json = JSON.stringify(inline);
70
+ if (compacted || payloadSha || json.length <= EVENT_LIMITS.payloadBytes) return { payloadJson: json, payloadSha: compacted && !payloadSha ? spill(Buffer.from(JSON.stringify(redacted))) : payloadSha };
71
+ const sha = spill(Buffer.from(JSON.stringify(redacted)));
72
+ return { payloadJson: JSON.stringify(spilledView(redacted, json.length, sha)), payloadSha: sha };
73
+ }
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { installRefResolver } from './ref-value.mjs';
5
+
6
+ // engine/db/ledger-open.mjs — how a ledger file is opened: bounded retry on a transient CANTOPEN, a reference-resolving handle.
7
+ const require=createRequire(import.meta.url);
8
+ export const need=(ok,message,code)=>{if(!ok)throw Object.assign(new Error(message),code?{code}:{});};
9
+ export const applyPragmas=(db,pragmas)=>db.exec(Object.entries(pragmas).map(([k,v])=>`PRAGMA ${k}=${v};`).join(' '));
10
+ const openSleep=ms=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,ms);
11
+ // SQLITE_CANTOPEN is transient on Windows while a concurrent process closes the WAL files: a short bounded retry.
12
+ const OPEN_RETRY_DELAYS_MS=[0,300,900];
13
+ const cantOpen=error=>/unable to open/i.test(String(error?.message??''));
14
+ export function openDb({file,busyTimeoutMs,journalMode='WAL',autoVacuum=false,label,pragmas}){
15
+ const {DatabaseSync}=require('node:sqlite');
16
+ need(typeof file==='string'&&file.trim(),`${label} needs a file`);
17
+ fs.mkdirSync(path.dirname(path.resolve(file)),{recursive:true});
18
+ let lastError;
19
+ for(const delay of OPEN_RETRY_DELAYS_MS){
20
+ if(delay)openSleep(delay);
21
+ let db;
22
+ try{
23
+ db=installRefResolver(new DatabaseSync(file,{timeout:busyTimeoutMs}));
24
+ // page_size/auto_vacuum only take on an empty database, before WAL and before the first table.
25
+ if(autoVacuum&&Number(db.prepare('PRAGMA page_count').get().page_count)===0)db.exec('PRAGMA page_size=4096; PRAGMA auto_vacuum=INCREMENTAL;');
26
+ const sqliteVersion=db.prepare('select sqlite_version() AS version').get().version;
27
+ const actual=String(db.prepare(`PRAGMA journal_mode=${journalMode}`).get().journal_mode).toUpperCase();
28
+ need(actual===String(journalMode).toUpperCase(),`${label}: SQLite selected journal mode ${actual}, expected ${journalMode} (a network or UNC path cannot hold WAL)`,'STARCI_LEDGER_NOT_WAL');
29
+ applyPragmas(db,pragmas);
30
+ return {db,sqliteVersion,journalMode:actual};
31
+ }catch(error){
32
+ try{db?.close();}catch{}
33
+ if(!cantOpen(error))throw error;
34
+ lastError=error;
35
+ }
36
+ }
37
+ throw lastError;
38
+ }
@@ -0,0 +1,87 @@
1
+ // Ledger path identity and ownership resolution; connection policy and caches remain in ledger.mjs.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import {sha256} from '../digest.mjs';
5
+ import {isUnderTempDir,localProjectsRoot,readMachine} from './machine.mjs';
6
+ import {isSpecRun} from '../../scripts/lib/env.mjs';
7
+ import {pathKey} from '../../scripts/lib/path-key.mjs';
8
+ import {byCodeUnit} from '../by-code-unit.mjs';
9
+ import {tempRoot} from '../temp-root.mjs';
10
+ /** Overrides the projects root (the directory holding <ledger_id>/runtime.sqlite) for this process tree; narrower than machine-db.mjs LOCAL_ROOT_ENV, which this still honors through starciLocalRoot when unset. */
11
+ export const PROJECTS_ROOT_ENV='STARCI_PROJECTS_ROOT';
12
+ const normDir=file=>pathKey(file);
13
+ /** <runtime root>/.runtime/projects (starciLocalRoot, itself overridable by STARCI_LOCAL_ROOT; a node --test process tree gets one under the OS temp directory). */
14
+ export const projectsRootFor=(env=process.env)=>{
15
+ if(env[PROJECTS_ROOT_ENV])return path.resolve(env[PROJECTS_ROOT_ENV]);
16
+ const root=localProjectsRoot(env);
17
+ if(isSpecRun(env)&&!isUnderTempDir(root,{env}))return path.join(tempRoot({env}),'starci-test-projects');
18
+ return root;
19
+ };
20
+ /** Repository identity: resolved/realpath, forward slashes, case-folded only on Windows. */
21
+ export const repoRootKey=repoRoot=>{let root=path.resolve(repoRoot);try{root=fs.realpathSync.native(root);}catch{}return normDir(root);};
22
+ /**
23
+ * The ledger id of a repository root until the machine registry (a3-2 machine.ledgers) resolves it: a name-based
24
+ * UUID of the normalized root, so the same root always names the same ledger and no side registry is needed.
25
+ */
26
+ const ledgerIdFromKey=key=>{
27
+ const h=sha256(`starci-ledger:${key}`);
28
+ return `${h.slice(0,8)}-${h.slice(8,12)}-5${h.slice(13,16)}-${(8|(Number.parseInt(h[16],16)&3)).toString(16)}${h.slice(17,20)}-${h.slice(20,32)}`;
29
+ };
30
+ export const ledgerIdForRepo=repoRoot=>ledgerIdFromKey(repoRootKey(repoRoot));
31
+ /** The registered runtime.sqlite of `repoRoot` in machine.ledgers (a3-2 resolveLedger), or null. */
32
+ function registeredLedgerFile(root,env,openReader){
33
+ let row;try{row=readMachine(m=>m.resolveLedger({repoRoot:root}),null,{env});}catch{return null;}
34
+ const file=row?.file&&row.state!=='retired'?path.resolve(row.file):null;
35
+ if(file&&fs.existsSync(file))assertLedgerRoot(file,root,openReader);
36
+ return file;
37
+ }
38
+ function assertLedgerRoot(file,root,openReader){
39
+ const db=openReader(file);let own;try{own=db.prepare("SELECT value FROM meta WHERE key='repo_root'").get()?.value;}finally{db.close();}
40
+ if(!own||repoRootKey(own)!==repoRootKey(root))throw Object.assign(new Error(`ledger-root-mismatch: ${file} belongs to another or unverified repository; preserve the database and resolve the owner binding with the owner`),{code:'STARCI_LEDGER_ROOT_MISMATCH'});
41
+ }
42
+ function unregisteredLedgerFile(root,env,openReader){
43
+ const base=projectsRootFor(env),current=path.join(base,ledgerIdForRepo(root),'runtime.sqlite');
44
+ if(fs.existsSync(current)){assertLedgerRoot(current,root,openReader);return current;}
45
+ return current;
46
+ }
47
+ /** Resolve the registry or exact owned store; the ledger owner supplies its verified read-only opener. */
48
+ export function resolveLedgerFile(root,{env,openReader}){
49
+ return registeredLedgerFile(root,env,openReader)??unregisteredLedgerFile(root,env,openReader);
50
+ }
51
+
52
+ const SYNTHETIC_LEDGER_ID=/^00000000-0000-4000-8000-[0-9a-f]{12}$/;
53
+ const fixtureRefused=message=>{throw new TypeError(`ledger-fixture-refused: ${message}`);};
54
+ const assertFixtureIdentity=fixture=>{
55
+ if(fixture.sqliteVersion!==undefined&&(typeof fixture.sqliteVersion!=='string'||!/^\d+\.\d+\.\d+(?:\.\d+)?$/.test(fixture.sqliteVersion)))fixtureRefused('sqliteVersion must be a dotted numeric version');
56
+ if(typeof fixture.ledgerId!=='string'||!SYNTHETIC_LEDGER_ID.test(fixture.ledgerId))fixtureRefused('ledgerId must be a synthetic UUID');
57
+ if(!Number.isSafeInteger(fixture.createdAt)||fixture.createdAt<=0||!Number.isFinite(new Date(fixture.createdAt).getTime()))fixtureRefused('createdAt must be a positive safe epoch millisecond');
58
+ const root=fixture.blobRoot;
59
+ if(typeof root!=='string'||!root||root.includes('\\')||root.trim()!==root||/[<>:"|?*\x00-\x1f]/.test(root)
60
+ ||path.posix.isAbsolute(root)||root.split('/').some(p=>!p||p==='.'||p==='..'))fixtureRefused('blobRoot must be a portable relative path');
61
+ };
62
+ const assertFixtureFile=file=>{
63
+ if(typeof file!=='string'||!path.isAbsolute(file)||path.basename(file)!=='runtime.sqlite')fixtureRefused('file must be an explicit absolute runtime.sqlite');
64
+ for(const suffix of ['','-wal','-shm'])if(fs.lstatSync(file+suffix,{throwIfNoEntry:false}))fixtureRefused('file and WAL/SHM must be absent');
65
+ const parent=path.dirname(file),stat=fs.lstatSync(parent,{throwIfNoEntry:false});
66
+ if(!stat?.isDirectory()||stat.isSymbolicLink()||fs.realpathSync.native(parent)!==path.resolve(parent))fixtureRefused('sample parent must be an existing physical directory');
67
+ };
68
+ /** Validate a fresh, initialization-only sample identity. Samples carry portable metadata,
69
+ * never a repository/machine binding; the ordinary project resolver is not bypassed. */
70
+ export function ledgerFixtureInit(fixture,{file,repoRoot,product,machine,mapped,checkpointer,now}){
71
+ if(fixture===null){
72
+ if(typeof file==='string'&&SYNTHETIC_LEDGER_ID.test(path.basename(path.dirname(path.resolve(file)))))fixtureRefused('synthetic identity is reserved for a sample');
73
+ return null;
74
+ }
75
+ if(!fixture||typeof fixture!=='object'||Array.isArray(fixture)
76
+ ||![Object.prototype,null].includes(Object.getPrototypeOf(fixture))
77
+ ||!['blobRoot,createdAt,ledgerId','blobRoot,createdAt,ledgerId,sqliteVersion'].includes(Object.keys(fixture).sort(byCodeUnit).join(',')))fixtureRefused('needs ledgerId, createdAt and blobRoot, and may state sqliteVersion');
78
+ assertFixtureIdentity(fixture);
79
+ if(repoRoot!==null||product!==null||machine!==null||mapped)fixtureRefused('a sample cannot bind a repository, product, machine or cached project path');
80
+ if(!checkpointer||now!==Date.now)fixtureRefused('initialization owns its frozen clock and requires a checkpointer');
81
+ assertFixtureFile(file);
82
+ return Object.freeze({...fixture,marker:'starci/basic-runtime-fixture@1'});
83
+ }
84
+ /** Refuse sample bytes as an operational ledger before opening a writer or beginning a transaction. */
85
+ export function assertOperationalLedger(meta){
86
+ if(meta.fixture||SYNTHETIC_LEDGER_ID.test(meta.ledger_id??''))throw new Error('ledger-fixture-read-only: preserve the sample; initialize a new live project store');
87
+ }