@starci/hfs 4.7.31 → 4.7.33

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 +12 -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 +54 -14
  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,80 @@
1
+ // supervisor-seat.mjs — what the Supervisor seat (a bound caller of role supervisor) may run, from the `supervisor` table of
2
+ // modules/kernel/command-policy.yaml. The PreToolUse guard and the PATH shims evaluate it (command-policy.mjs policyVerdict): the
3
+ // seat reads, answers the items of its menu with `starci supervisor decide`, records a runtime defect for Debug and runs the
4
+ // collectors it owns. Every refusal names the status and decision verbs without running them.
5
+ import { plainWords } from '../lib/menu-parts.mjs';
6
+ import { refusal } from './rights.mjs';
7
+ import { runtimeChangeRefusal, RUNTIME_CHANGE_CODE } from '../machine/runtime-change.mjs';
8
+
9
+ const OPTION = /^--?([A-Za-z][\w-]*)(?:=(.*))?$/;
10
+
11
+ /** The option names and the values of the options that carry one (`--item x` or `--item=x`) of a call. */
12
+ function optionsOf(args) {
13
+ const names = new Set();
14
+ const values = new Map();
15
+ args.map(String).forEach((word, index, all) => {
16
+ const hit = OPTION.exec(word);
17
+ if (!hit) return;
18
+ names.add(hit[1]);
19
+ const next = all[index + 1];
20
+ const value = hit[2] ?? (next?.startsWith('-') === false ? next : undefined);
21
+ if (value !== undefined) values.set(hit[1], value);
22
+ });
23
+ return { names, values };
24
+ }
25
+
26
+ /** The first three non-option words of a starci call: its group, verb and action. */
27
+ const listed = (table, group, verb) => (table?.verbs?.[group] ?? []).includes(verb);
28
+
29
+ /** The reason a listed verb is still refused for its action or options, or null. */
30
+ function narrowReason(table, { group, verb, action }, options) {
31
+ const actions = table.actions?.[verb];
32
+ if (actions?.includes(action) === false) return `starci ${[group, verb, action].filter(Boolean).join(' ')} is not an action of the Supervisor seat (${actions.join(', ')})`;
33
+ const blocked = (table['read-flags']?.[verb] ?? []).find((flag) => options.names.has(flag));
34
+ if (blocked) return `starci ${group} ${verb} --${blocked} changes state: the menu's decision verb does it`;
35
+ if (verb === 'actions' && action === 'record' && !new RegExp(table['record-items']).test(options.values.get('item') ?? '')) {
36
+ return 'starci supervisor actions record is allowed for a runtime defect only: --item runtime-defect:<cause>';
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /** Whether the seat may run `starci <group> <verb> [action]` with these options: {allowed, reason?}. */
42
+ function supervisorSeatAllows(table, words, args = []) {
43
+ const [group, verb, action] = words;
44
+ if (!listed(table, group, verb)) return { allowed: false, reason: `starci ${group} ${verb} is not a verb of the Supervisor seat` };
45
+ const reason = narrowReason(table, { group, verb, action }, optionsOf(args));
46
+ return reason ? { allowed: false, reason } : { allowed: true };
47
+ }
48
+
49
+ const useOf = (table) => table.use;
50
+
51
+ /** A runtime-change verb the seat tried (land, a fix worker): the Debug code, with the menu as the way on. */
52
+ function runtimeChangeVerdict({ table, words, text }) {
53
+ const change = runtimeChangeRefusal(words);
54
+ if (!change) return null;
55
+ const use = useOf(table);
56
+ return refusal(RUNTIME_CHANGE_CODE, text, change.reason, `${change.remedy}\n${use}`, use);
57
+ }
58
+
59
+ /**
60
+ * The refusal of a call the Supervisor seat may not make, or null when it may run it. A program other than starci passes only as a
61
+ * pure read or as a program whose read forms the general policy judges (orca lifecycle, git, npm); the rest is refused.
62
+ */
63
+ export function supervisorSeatVerdict({ policy, program, args, text }) {
64
+ const table = policy?.supervisor;
65
+ if (!table) return null;
66
+ if (program === 'starci') {
67
+ const words = plainWords(args, 3);
68
+ const changed = runtimeChangeVerdict({ table, words, text });
69
+ if (changed) return changed;
70
+ const verdict = supervisorSeatAllows(table, words, args);
71
+ if (verdict.allowed) return null;
72
+ const use = useOf(table);
73
+ return refusal('SUPERVISOR_USE_DECIDE', text, `${verdict.reason}: the runtime runs it, and the Supervisor answers the items of its menu`, use, use);
74
+ }
75
+ const reads = new Set([...(policy.read ?? []), ...Object.keys(policy['guarded-read'] ?? {}), ...(table.defer ?? [])].map(String));
76
+ const denied = new Set((table['deny-programs'] ?? []).map(String));
77
+ if (reads.has(program) && !denied.has(program)) return null;
78
+ const use = useOf(table);
79
+ return refusal('SUPERVISOR_STARCI_ONLY', text, `the Supervisor seat runs starci verbs and pure reads: ${program} is neither`, use, use);
80
+ }
@@ -0,0 +1,81 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { spawnNode } from '../api/node/spawn-node.mjs';
3
+ import { killTree } from '../api/process/kill-tree.mjs';
4
+ import { HOOK_INPUT_LIMITS } from './hook-io.mjs';
5
+
6
+ const entry = fileURLToPath(new URL('./command-guard.mjs', import.meta.url));
7
+
8
+ /** Request forced teardown of this call's verifier tree; a failed tree stop is reported after the direct child is stopped. */
9
+ function stopVerifier(child, teardownTimeoutMs) {
10
+ if (!Number.isInteger(child?.pid) || child.pid <= 0 || child.exitCode !== null) return;
11
+ let failure;
12
+ if (process.platform === 'win32') {
13
+ const stopped = killTree(child.pid, { timeoutMs: teardownTimeoutMs });
14
+ if (!stopped.ok) {
15
+ child.kill('SIGKILL');
16
+ const detail = stopped.output || `exit ${stopped.status}`;
17
+ failure = new Error(`verifier tree stop failed (${detail})`);
18
+ }
19
+ } else {
20
+ try { process.kill(-child.pid, 'SIGKILL'); }
21
+ catch (error) {
22
+ if (error.code !== 'ESRCH') {
23
+ child.kill('SIGKILL');
24
+ failure = error;
25
+ }
26
+ }
27
+ }
28
+ if (child.connected) child.disconnect();
29
+ child.unref();
30
+ if (failure) throw failure;
31
+ }
32
+
33
+ /**
34
+ * Evaluate a hook in an owned child so synchronous policy work cannot block its caller's deadline.
35
+ * The promise resolves only after the child sends a decision and exits; startup, IPC and deadline failures reject.
36
+ * A deadline requests bounded forced teardown of that child's tree and never accepts a late decision.
37
+ */
38
+ export function runHookVerdict({ input, root, bindings, env = process.env, start = spawnNode,
39
+ timeoutMs = HOOK_INPUT_LIMITS.verdictTimeoutMs, teardownTimeoutMs = HOOK_INPUT_LIMITS.teardownTimeoutMs } = {}) {
40
+ return new Promise((resolve, reject) => {
41
+ const deadline = performance.now() + timeoutMs;
42
+ let child;
43
+ let settled = false;
44
+ let response;
45
+ const finish = (error, decision) => {
46
+ if (settled) return;
47
+ // Event-loop delays can deliver close before the expired timer; elapsed time still owns admission.
48
+ if (!error && performance.now() >= deadline) error = new Error(`hook verdict deadline exceeded (${timeoutMs} ms)`);
49
+ settled = true;
50
+ clearTimeout(timer);
51
+ if (error) {
52
+ try { stopVerifier(child, teardownTimeoutMs); }
53
+ catch (cleanupError) {
54
+ child?.channel?.unref();
55
+ child?.unref();
56
+ reject(new Error(`${error.message}; verifier cleanup failed (${cleanupError.message})`));
57
+ return;
58
+ }
59
+ reject(error);
60
+ } else resolve(decision);
61
+ };
62
+ const timer = setTimeout(() => finish(new Error(`hook verdict deadline exceeded (${timeoutMs} ms)`)), timeoutMs);
63
+ try {
64
+ child = start([entry], { env, detached: process.platform !== 'win32', stdio: ['ignore', 'ignore', 'ignore', 'ipc'] });
65
+ child.once('error', (error) => finish(error));
66
+ child.on('message', (message) => {
67
+ if (settled || response) return;
68
+ if (message?.ok !== true || !Object.hasOwn(message, 'decision')) {
69
+ finish(new Error(String(message?.error ?? 'hook verifier returned an invalid decision')));
70
+ return;
71
+ }
72
+ response = message;
73
+ });
74
+ child.once('close', (code, signal) => {
75
+ if (response && code === 0 && !signal) finish(null, response.decision);
76
+ else finish(new Error(`hook verifier exited before its decision (exit ${code}, signal ${signal ?? 'none'})`));
77
+ });
78
+ child.send({ input, root, bindings }, (error) => { if (error) finish(error); });
79
+ } catch (error) { finish(error); }
80
+ });
81
+ }
@@ -3,7 +3,7 @@
3
3
  * HFS_UNUSED_EXPORT an export of an owner's public entry that no production file outside the owner imports is dead.
4
4
  * HFS_UNUSED_FILE a production source file that no root reaches is dead. Roots: the files an app slot requires
5
5
  * (main.ts, app.module.ts), every route file (slot tier `route`), the public entry of every package,
6
- * and an api app operation table `operations.ts` (the contract emit reads it by path),
6
+ * the declared API app's registered operation contract consumed by app emit,
7
7
  * and a source file a framework config of the app names by a relative string literal
8
8
  * (next.config.ts pointing next-intl at its request config). Specs and tests are not in the graph,
9
9
  * so a file only a spec imports is dead, on purpose. Type-only imports reach a file.
@@ -42,14 +42,13 @@ import { treeOf } from './required-files.mjs';
42
42
  import { globExpression } from '../../lib/glob.mjs';
43
43
  import { stagedConsumerSources } from './staged-consumers.mjs';
44
44
  import { bindingsOf } from './dead-exports-bindings.mjs';
45
+ import { operationContractRoots } from './operation-contract-roots.mjs';
45
46
 
46
47
  export const DEAD_EXPORT_RULE_IDS = ['HFS_UNUSED_EXPORT', 'HFS_UNUSED_FILE'];
47
48
 
48
49
  const PACKAGE_ENTRIES = ['src/index.ts', 'src/index.tsx', 'index.ts', 'index.tsx'];
49
50
  const OWNER_ENTRIES = ['index.ts', 'index.tsx'];
50
51
  const CHAIN_DEPTH = 6;
51
- /** The file of an api app that the contract emit reads by path for the typed operation table (BE-OPERATIONS-1): no import reaches it, the emit does. */
52
- const OPERATION_TABLE_FILE = "operations.ts";
53
52
  /** Tiers whose files are not production source that an app serves: judged by no dead-file rule. */
54
53
  const UNJUDGED_TIERS = new Set(['none', 'e2e', 'fixtures']);
55
54
  const CONFIG_EXTENSIONS = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '/index.ts', '/index.tsx', '/index.js'];
@@ -139,15 +138,15 @@ const addFrameworkConfigRoots = (graph, config, roots) => {
139
138
  for (const app of config.apps ?? []) addAppConfigRoots(graph, config, app, roots);
140
139
  };
141
140
 
142
- /** The files the graph serves from: what an app slot requires, every route file, every package entry, every config-named file. */
143
- function rootFiles(graph, config) {
141
+ /** Roots are required app files, registered operation contracts, routes, package entries and config-named files. */
142
+ function rootFiles(graph, config, context) {
144
143
  const roots = new Set();
145
144
  for (const [rel, node] of graph.files) {
146
145
  if (node.tier === 'route') { roots.add(rel); continue; }
147
146
  if (!node.owner || node.tier !== 'app') continue;
148
147
  const slot = graph.resolver.slot(node.owner.slot);
149
148
  const inside = rel.slice(withoutSlash(node.owner.root).length + 1);
150
- if ((slot?.requires ?? []).includes(inside) || inside === OPERATION_TABLE_FILE) roots.add(rel);
149
+ if ((slot?.requires ?? []).includes(inside)) roots.add(rel);
151
150
  }
152
151
  for (const [key, owner] of graph.ownerRoots) {
153
152
  if (owner.tier !== 'package') continue;
@@ -156,6 +155,7 @@ function rootFiles(graph, config) {
156
155
  }
157
156
  // A framework config of an app (slot fe.app.next) names sources by string, never by import.
158
157
  addFrameworkConfigRoots(graph, config, roots);
158
+ for (const entry of operationContractRoots(context, graph, config)) roots.add(entry);
159
159
  return roots;
160
160
  }
161
161
 
@@ -163,12 +163,12 @@ const unusedFileViolation = (rel, node) => ({
163
163
  ruleId: 'HFS_UNUSED_FILE',
164
164
  path: rel, line: 1, column: 1,
165
165
  slot: node.slot, owner: node.owner?.root ?? null,
166
- message: `${rel} is not reached from any root (an app main.ts or app.module.ts, a route file, a package entry): nothing imports it, so no process runs it. Delete the file, or import it where it is used (a spec alone does not count).`,
166
+ message: `${rel} is not reached from any root (an app main.ts or app.module.ts, a registered operation contract, a route file, a package entry): nothing consumes it. Delete the file, or import it where it is used (a spec alone does not count).`,
167
167
  });
168
168
 
169
169
  /** HFS_UNUSED_FILE: production files no root reaches through runtime or type-only imports and re-exports. */
170
- function deadFiles(graph, config) {
171
- const roots = rootFiles(graph, config);
170
+ function deadFiles(graph, config, context) {
171
+ const roots = rootFiles(graph, config, context);
172
172
  const forward = Map.groupBy(graph.edges, edge => edge.from);
173
173
  const reached = new Set(roots);
174
174
  const queue = [...roots];
@@ -355,7 +355,7 @@ export function checkDeadExports({ context, graph, config }) {
355
355
  const edgesTo = edgeIndex([...graph.edges, ...testConsumerEdges({ context, graph, config })]);
356
356
  const analysis = { ...context, graph, edgesTo, namespaceCheckers: new WeakMap(), namespaceUses: new WeakMap() };
357
357
  const exports = ownerExportFindings(analysis, config);
358
- const files = deadFiles(graph, config);
358
+ const files = deadFiles(graph, config, context);
359
359
  const dead = exports.violations.length;
360
360
  exports.violations.push(...files.violations);
361
361
  return { violations: exports.violations, coverage: { status: 'checked', owners: exports.owners, exports: exports.exports, dead, files: files.judged, roots: files.roots, unusedFiles: files.violations.length } };
@@ -51,7 +51,7 @@ export function valueObjectSubjects(ts, sourceFile) {
51
51
  }
52
52
 
53
53
  /** The value of one literal object property. */
54
- const propertyValue = (ts, object, key) => {
54
+ export const propertyValue = (ts, object, key) => {
55
55
  const property = object.properties.find(item => keyOf(ts, item) === key);
56
56
  return property && ts.isPropertyAssignment(property) ? property.initializer : null;
57
57
  };
@@ -0,0 +1,53 @@
1
+ /** The app emitter consumes the API app's declared operation table without importing it from app.module.ts. */
2
+ import path from 'node:path';
3
+ import { hasAnyFlag } from '../../lib/ts-ast.mjs';
4
+
5
+ const actualSymbol = (ts, checker, symbol) => symbol && hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol;
6
+
7
+ /** Follow declaration aliases while retaining the actual expression rather than an asserted type. */
8
+ const initializerOf = (ts, checker, node, seen = new Set()) => {
9
+ if (!node || seen.has(node)) return null;
10
+ seen.add(node);
11
+ if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
12
+ || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node)) return initializerOf(ts, checker, node.expression, seen);
13
+ if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return node;
14
+ const declaration = actualSymbol(ts, checker, checker.getSymbolAtLocation(node))?.valueDeclaration;
15
+ return declaration?.initializer ? initializerOf(ts, checker, declaration.initializer, seen) : node;
16
+ };
17
+
18
+ /** A same-named local function cannot declare the platform operation contract. */
19
+ const isOperationDefiner = (ts, checker, graph, config, expression, seen = new Set()) => {
20
+ const symbol = actualSymbol(ts, checker, checker.getSymbolAtLocation(expression));
21
+ if (!symbol || seen.has(symbol)) return false;
22
+ seen.add(symbol);
23
+ return (symbol?.declarations ?? []).some(declaration => {
24
+ const relative = path.relative(config.root, declaration.getSourceFile().fileName).split(path.sep).join('/');
25
+ const owner = graph.resolver.ownerOf(relative);
26
+ if (declaration.name?.text === 'defineOperations' && owner?.slot === 'be.platform' && path.posix.basename(owner.root) === 'operations') return true;
27
+ const alias = declaration.initializer;
28
+ return Boolean(alias && (ts.isIdentifier(alias) || ts.isPropertyAccessExpression(alias)) && isOperationDefiner(ts, checker, graph, config, alias, seen));
29
+ });
30
+ };
31
+
32
+ /** True only for an exported, nonempty registration by the canon platform capability. */
33
+ const declaresOperations = (context, graph, config, sourceFile) => {
34
+ const { ts } = context;
35
+ const checker = context.checkerFor?.(sourceFile.fileName);
36
+ const moduleSymbol = checker?.getSymbolAtLocation(sourceFile);
37
+ if (!moduleSymbol) return false;
38
+ const exported = checker.getExportsOfModule(moduleSymbol).find(symbol => symbol.getName() === 'OPERATIONS');
39
+ const declaration = actualSymbol(ts, checker, exported)?.valueDeclaration;
40
+ const registration = initializerOf(ts, checker, declaration?.initializer);
41
+ if (!registration || !ts.isCallExpression(registration) || !isOperationDefiner(ts, checker, graph, config, registration.expression)) return false;
42
+ const entries = initializerOf(ts, checker, registration.arguments[0]);
43
+ return entries !== null && ts.isObjectLiteralExpression(entries) && checker.getPropertiesOfType(checker.getTypeAtLocation(entries)).length > 0;
44
+ };
45
+
46
+ /** Only the emitter's exact entry of a declared API app is a contract root; workers and arbitrary app files remain judged. */
47
+ export function operationContractRoots(context, graph, config) {
48
+ return (config.apps ?? []).filter(app => app.kind === 'api').flatMap(app => {
49
+ const relative = `apps/${app.name}/src/operations.ts`;
50
+ const node = graph.files.get(relative);
51
+ return node?.tier === 'app' && node.slot === 'be.app.api' && declaresOperations(context, graph, config, node.sourceFile) ? [relative] : [];
52
+ });
53
+ }
@@ -13,7 +13,7 @@ import { byCodeUnit } from '../../lib/list.mjs';
13
13
 
14
14
  export const REACHABILITY_RULE_IDS = ['BE_FEATURE_NOT_COMPOSED', 'FE_OWNER_REACHABLE', 'FE_HREF_RESOLVES'];
15
15
 
16
- const BE_CAPABILITY_SLOTS = new Set(['be.domain', 'be.integrations', 'be.platform']);
16
+ const BE_CAPABILITY_SLOTS = new Set(['be.domain', 'be.integrations', 'be.platform', 'be.platform.logging']);
17
17
  const TEST_FILE = /(?:\.(?:spec|test|stories|e2e-spec)\.[cm]?[jt]sx?$)|(?:(?:^|\/)__tests__\/)/;
18
18
  const ROUTE_FILE = /^(?:page|route)\.(?:tsx|ts|jsx|js)$/;
19
19
  const PLACEHOLDER = '\u0000';
@@ -3,6 +3,7 @@ import { analyzeSql, HOLE } from './sql-tokens.mjs';
3
3
  import { machineKit } from './machine-ast.mjs';
4
4
  import { contextModelOf } from './context-map.mjs';
5
5
  import { supabaseTablesOf } from './supabase-tables.mjs';
6
+ import { supabaseUniqueSetsOf } from './supabase-keys.mjs';
6
7
 
7
8
  /**
8
9
  * R86 `sql-owner` (BE_SQL_TABLE_OWNER). Every `sql` tagged template (the tag declared in platform/database) of a
@@ -14,10 +15,10 @@ import { supabaseTablesOf } from './supabase-tables.mjs';
14
15
  * to the table owner's public entry); a table of another context is refused (cross-context data comes only by events);
15
16
  * - every table it names is declared by some entity under TypeORM authority, or by the generated
16
17
  * `Database['public']['Tables']` contract under Supabase authority;
17
- * - a SELECT carries LIMIT, or constrains the primary key or a unique key of its first table with `=`, or selects only
18
- * aggregates without GROUP BY.
18
+ * - a SELECT carries LIMIT, or reads a single relation under a complete primary/unique-key equality to scalar values,
19
+ * or selects only proven scalar aggregates without GROUP BY. Inferred bounds reject multiplying projections and set operations.
19
20
  * Table positions that hold a `${...}` substitution (SqlIdent) are dynamic; they are counted in the coverage and judged by
20
- * nobody. Joined tables are not part of the bound: a to-many join under a key-constrained first table is a review point.
21
+ * nobody. A join needs an explicit result bound because key constraints on its first relation do not bound joined rows.
21
22
  */
22
23
  export const SQL_OWNER_RULE_IDS = ['BE_SQL_TABLE_OWNER'];
23
24
 
@@ -132,7 +133,8 @@ const capabilityTable = root => path.posix.basename(root).replaceAll('-', '_').t
132
133
  /** Supabase declarations, with an owner when the generated table name matches a capability's persistence folder. */
133
134
  function supabaseDeclarations(graph, appRoot, ts) {
134
135
  const generated = supabaseTablesOf(appRoot, ts);
135
- const tables = new Map([...generated].map(table => [table, { table, owner: null, rel: null, uniqueSets: [] }]));
136
+ const keys = supabaseUniqueSetsOf(appRoot);
137
+ const tables = new Map([...generated].map(table => [table, { table, owner: null, rel: null, uniqueSets: keys.get(table) ?? [] }]));
136
138
  for (const file of graph.files.values()) {
137
139
  if (file.slot !== 'be.persistence' || !file.owner) continue;
138
140
  const key = capabilityTable(file.owner.root);
@@ -142,6 +144,12 @@ function supabaseDeclarations(graph, appRoot, ts) {
142
144
  return tables;
143
145
  }
144
146
 
147
+ /** A capability owns the table named for it and the tables that start with its name and an underscore (`realtime`, `realtime_events`). */
148
+ const isOwnTable = (table, ownTable) => {
149
+ const name = table.toLowerCase();
150
+ return name === ownTable || name.startsWith(`${ownTable}_`);
151
+ };
152
+
145
153
  function reportSqlWrite({ tables, supabase, file, ownTable, report }, write) {
146
154
  const declaration = tables.get(write.table.toLowerCase());
147
155
  if (!declaration) {
@@ -149,7 +157,7 @@ function reportSqlWrite({ tables, supabase, file, ownTable, report }, write) {
149
157
  ? `SQL writes table ${write.table}, which Database['public']['Tables'] does not declare; regenerate supabase/types/database.types.ts from the migrations.`
150
158
  : `SQL writes table ${write.table}, which no @Entity declares; declare the entity in the owning capability's persistence/entities.`, { table: write.table });
151
159
  } else if (supabase) {
152
- if (write.table.toLowerCase() === ownTable) return;
160
+ if (isOwnTable(write.table, ownTable) && (declaration.owner === null || declaration.owner === file.owner.root)) return;
153
161
  const ownerNote = declaration.owner ? `, owned by ${declaration.owner}` : '';
154
162
  report(`SQL in ${file.owner.root} writes table ${write.table}${ownerNote}; under Supabase schema authority a capability writes only its own table (${ownTable}), named for that capability.`, { table: write.table, ...(declaration.owner ? { tableOwner: declaration.owner } : {}) });
155
163
  } else if (declaration.owner !== file.owner.root) {
@@ -208,7 +216,10 @@ function inspectSqlTag(input, state, file, checker, node) {
208
216
  seen.add(key);
209
217
  state.violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
210
218
  };
211
- const result = analyzeSql(text, { uniqueSetsOf: table => tables.get(table.toLowerCase())?.uniqueSets ?? null });
219
+ const result = analyzeSql(text, { uniqueSetsOf: (table, schema) => {
220
+ if (supabase && ((schema && schema !== 'public') || table !== table.toLowerCase())) return null;
221
+ return tables.get(table.toLowerCase())?.uniqueSets ?? null;
222
+ } });
212
223
  state.dynamic += result.dynamic;
213
224
  const ownTable = capabilityTable(file.owner.root);
214
225
  reportSqlWrites({ result, tables, supabase, file, ownTable, report });
@@ -0,0 +1,105 @@
1
+ /** Token forms shared by SQL relation scanning and conservative singleton-value proofs. */
2
+
3
+ /** Whether a token can spell an SQL identifier. */
4
+ export const isName = token => token && (token.t === 'word' || token.t === 'ident');
5
+ /** The identifier spelling after SQL word folding. */
6
+ export const nameOf = token => (token.t === 'word' ? token.v.toLowerCase() : token.v);
7
+ /** Whether a token is the named SQL keyword. */
8
+ export const isWord = (token, up) => token?.t === 'word' && token.up === up;
9
+
10
+ // The scalar-cast grammar proves only these builtin targets, unqualified or explicitly under pg_catalog.
11
+ const SCALAR_CAST_TYPES = new Set(['uuid', 'text']);
12
+
13
+ /** Depth of each token (the depth before it when it is `(`, after it when it is `)`). */
14
+ export function depths(tokens) {
15
+ let depth = 0;
16
+ return tokens.map(token => {
17
+ if (token.v === ')' && token.t === 'punct') depth -= 1;
18
+ const here = depth;
19
+ if (token.v === '(' && token.t === 'punct') depth += 1;
20
+ return here;
21
+ });
22
+ }
23
+
24
+ /** The closing parenthesis of a token group, or the final token when unmatched. */
25
+ export function matching(tokens, open) {
26
+ let depth = 0;
27
+ for (let i = open; i < tokens.length; i += 1) {
28
+ if (tokens[i].t !== 'punct') continue;
29
+ if (tokens[i].v === '(') depth += 1;
30
+ else if (tokens[i].v === ')') { depth -= 1; if (depth === 0) return i; }
31
+ }
32
+ return tokens.length - 1;
33
+ }
34
+
35
+ function unparenthesized(tokens) {
36
+ let part = tokens;
37
+ while (part[0]?.v === '(' && part.at(-1)?.v === ')' && matching(part, 0) === part.length - 1) part = part.slice(1, -1);
38
+ return part;
39
+ }
40
+
41
+ function keyColumn(tokens, qualifiers) {
42
+ const part = unparenthesized(tokens);
43
+ if (singletonValue(part)) return null;
44
+ if (part.length === 1 && isName(part[0])) return nameOf(part[0]);
45
+ if (part.length === 3 && isName(part[0]) && part[1].v === '.' && isName(part[2]) && qualifiers.has(nameOf(part[0]))) return nameOf(part[2]);
46
+ return null;
47
+ }
48
+
49
+ function scalarCastType(tokens) {
50
+ if (tokens.length === 1 && isName(tokens[0])) return SCALAR_CAST_TYPES.has(nameOf(tokens[0]));
51
+ return tokens.length === 3 && isName(tokens[0]) && nameOf(tokens[0]) === 'pg_catalog' && tokens[1].v === '.'
52
+ && isName(tokens[2]) && SCALAR_CAST_TYPES.has(nameOf(tokens[2]));
53
+ }
54
+
55
+ /** SQL CAST is a scalar wrapper; its operand is still inspected separately for row dependence or set-returning calls. */
56
+ function castParts(tokens, start = 0) {
57
+ if (!isWord(tokens[start], 'CAST') || tokens[start + 1]?.v !== '(') return null;
58
+ const end = matching(tokens, start + 1);
59
+ if (tokens[end]?.v !== ')') return null;
60
+ const inside = tokens.slice(start + 2, end);
61
+ const level = depths(inside);
62
+ const separator = inside.findIndex((token, index) => level[index] === 0 && isWord(token, 'AS'));
63
+ if (separator < 1 || separator === inside.length - 1) return null;
64
+ return { value: inside.slice(0, separator), type: inside.slice(separator + 1), next: end + 1 };
65
+ }
66
+
67
+ /** A scalar atom, optionally cast through the supported builtin grammar, proves a row-independent equality value. */
68
+ function singletonValue(tokens) {
69
+ const part = unparenthesized(tokens);
70
+ if (part.length === 2 && ['+', '-'].includes(part[0].v) && part[1].t === 'number') return true;
71
+ if (part.length === 1) {
72
+ const [token] = part;
73
+ return ['param', 'string', 'number'].includes(token.t) || isWord(token, 'NULL') || isWord(token, 'TRUE') || isWord(token, 'FALSE');
74
+ }
75
+ const cast = castParts(part);
76
+ if (cast?.next === part.length && scalarCastType(cast.type)) return singletonValue(cast.value);
77
+ const level = depths(part);
78
+ for (let index = part.length - 1; index > 0; index -= 1) {
79
+ if (level[index] === 0 && part[index].v === '::') return scalarCastType(part.slice(index + 1)) && singletonValue(part.slice(0, index));
80
+ }
81
+ return false;
82
+ }
83
+
84
+ /** Key columns equated to proven row-independent singleton values in either operand order. */
85
+ export function equalityColumns(conjunct, qualifiers) {
86
+ const found = new Set();
87
+ const part = unparenthesized(conjunct);
88
+ const level = depths(part);
89
+ const equals = part.findIndex((token, index) => level[index] === 0 && token.t === 'punct' && token.v === '=');
90
+ if (equals < 0) return found;
91
+ const left = part.slice(0, equals);
92
+ const right = part.slice(equals + 1);
93
+ const leftColumn = keyColumn(left, qualifiers);
94
+ const rightColumn = keyColumn(right, qualifiers);
95
+ if (leftColumn && singletonValue(right)) found.add(leftColumn);
96
+ if (rightColumn && singletonValue(left)) found.add(rightColumn);
97
+ return found;
98
+ }
99
+
100
+ /** Whether a call position is SQL CAST through the supported scalar type grammar. */
101
+ export function scalarCastAt(tokens, index) {
102
+ if (tokens[index - 1]?.v === '.') return false;
103
+ const cast = castParts(tokens, index);
104
+ return Boolean(cast && scalarCastType(cast.type));
105
+ }
@@ -5,6 +5,8 @@
5
5
  * A substitution of the `sql` tag (a `${...}` hole, the SqlIdent form) is an opaque token: a table position that holds
6
6
  * one is counted as dynamic and judged by nobody.
7
7
  */
8
+ import { depths, equalityColumns, isName, isWord, matching, nameOf, scalarCastAt } from './sql-token-forms.mjs';
9
+
8
10
  export const HOLE = '';
9
11
 
10
12
  const isSpace = char => /\s/u.test(char);
@@ -129,10 +131,6 @@ const NOT_A_WRITE_BEFORE_UPDATE = new Set(['FOR', 'DO', 'ON', 'KEY', 'SHARE', 'O
129
131
  const WHERE_END = new Set(['GROUP', 'ORDER', 'HAVING', 'LIMIT', 'OFFSET', 'UNION', 'INTERSECT', 'EXCEPT', 'FOR', 'WINDOW', 'RETURNING', 'FETCH']);
130
132
  const SYSTEM_SCHEMAS = new Set(['pg_catalog', 'information_schema']);
131
133
 
132
- const isName = token => token && (token.t === 'word' || token.t === 'ident');
133
- const nameOf = token => (token.t === 'word' ? token.v.toLowerCase() : token.v);
134
- const isWord = (token, up) => token?.t === 'word' && token.up === up;
135
-
136
134
  /**
137
135
  * The table reference at `start`: {parts, table, alias, next}, {dynamic: true}, or null (a subquery, a function, nothing).
138
136
  * `next` is the index after the reference and its alias.
@@ -223,27 +221,6 @@ function cteNames(tokens) {
223
221
  return names;
224
222
  }
225
223
 
226
- /** Depth of each token (the depth before it when it is `(`, after it when it is `)`). */
227
- function depths(tokens) {
228
- let depth = 0;
229
- return tokens.map(token => {
230
- if (token.v === ')' && token.t === 'punct') depth -= 1;
231
- const here = depth;
232
- if (token.v === '(' && token.t === 'punct') depth += 1;
233
- return here;
234
- });
235
- }
236
-
237
- function matching(tokens, open) {
238
- let depth = 0;
239
- for (let i = open; i < tokens.length; i += 1) {
240
- if (tokens[i].t !== 'punct') continue;
241
- if (tokens[i].v === '(') depth += 1;
242
- else if (tokens[i].v === ')') { depth -= 1; if (depth === 0) return i; }
243
- }
244
- return tokens.length - 1;
245
- }
246
-
247
224
  function isAggregateItem(item) {
248
225
  for (let i = 0; i < item.length; i += 1) {
249
226
  if (item[i].t === 'word' && AGGREGATES.has(item[i].up) && item[i + 1]?.v === '(') {
@@ -257,28 +234,11 @@ function isAggregateItem(item) {
257
234
  /** The unique-key columns `columns` (lowercase Set of `=`-constrained column names) satisfy for one of `uniqueSets`. */
258
235
  const covers = (uniqueSets, columns) => uniqueSets.some(set => set.length > 0 && set.every(column => columns.has(column)));
259
236
 
260
- function equalityColumns(conjunct, qualifiers) {
261
- const found = new Set();
262
- const name = (from, to) => {
263
- const part = conjunct.slice(from, to);
264
- if (part.length === 1 && isName(part[0])) return nameOf(part[0]);
265
- if (part.length === 3 && isName(part[0]) && part[1].v === '.' && isName(part[2]) && qualifiers.has(nameOf(part[0]))) return nameOf(part[2]);
266
- return null;
267
- };
268
- const equals = conjunct.findIndex(token => token.t === 'punct' && token.v === '=');
269
- if (equals < 0) return found;
270
- const left = name(0, equals);
271
- const rightStart = conjunct[equals + 1];
272
- if (left && !(rightStart?.t === 'word' && ['ANY', 'ALL', 'SOME'].includes(rightStart.up))) found.add(left);
273
- const right = name(equals + 1, conjunct.length);
274
- if (right) found.add(right);
275
- return found;
276
- }
277
-
278
237
  /**
279
238
  * Judge one statement that reads rows (no INSERT/UPDATE/DELETE/MERGE at its own depth): `{bounded, basis}`. Bounded is
280
239
  * a LIMIT/FETCH at the statement depth, no FROM at all (one row), only aggregates without GROUP BY, or an equality on
281
- * every column of the primary key or of one unique key of the first table of FROM.
240
+ * every column of the primary key or of one unique key of a single FROM relation. Unknown projection calls can
241
+ * multiply rows even when that relation contributes at most one, so they need an explicit statement bound.
282
242
  */
283
243
  function aggregateSelectItems(tokens, level, select, from) {
284
244
  const items = [];
@@ -338,16 +298,42 @@ function isUniqueBounded(tokens, level, from, primary, uniqueSets) {
338
298
  return Boolean(columns) && covers(uniqueSets, columns);
339
299
  }
340
300
 
301
+ function singleFromRelation(tokens, level, from) {
302
+ for (let index = from + 1; index < tokens.length; index += 1) {
303
+ if (level[index] !== 0) continue;
304
+ if (isWord(tokens[index], 'WHERE') || (tokens[index].t === 'word' && WHERE_END.has(tokens[index].up))) break;
305
+ if (isWord(tokens[index], 'JOIN') || (tokens[index].t === 'punct' && tokens[index].v === ',')) return false;
306
+ }
307
+ return true;
308
+ }
309
+
310
+ /** Plain known aggregate calls are scalar; other calls, including wrappers around aggregates, may return sets. */
311
+ function unprovenProjectionCall(tokens, level, select, from) {
312
+ const end = from < 0 ? tokens.length : from;
313
+ for (let index = select + 1; index < end; index += 1) {
314
+ const token = tokens[index];
315
+ if (level[index] === 0 && (isWord(token, 'WHERE') || (token.t === 'word' && WHERE_END.has(token.up)))) break;
316
+ if (!isName(token) || tokens[index + 1]?.v !== '(') continue;
317
+ if (scalarCastAt(tokens, index)) continue;
318
+ if (token.t !== 'word' || !AGGREGATES.has(token.up) || tokens[index - 1]?.v === '.') return true;
319
+ }
320
+ return false;
321
+ }
322
+
341
323
  function selectBound(tokens, level, uniqueSetsOf, cte) {
342
324
  if (findTopWord(tokens, level, 'LIMIT') >= 0 || findTopWord(tokens, level, 'FETCH') >= 0) return { bounded: true, basis: 'limit' };
343
325
  const select = findTopWord(tokens, level, 'SELECT');
344
326
  const from = findTopWord(tokens, level, 'FROM', select);
345
- if (from < 0) return { bounded: true, basis: 'no from' };
346
- if (!hasTopGroupBy(tokens, level) && aggregateSelectItems(tokens, level, select, from).every(isAggregateItem)) return { bounded: true, basis: 'aggregate' };
347
- const primary = tableRef(tokens, from + 1);
348
- if (!primary || primary.dynamic || cte.has(primary.table)) return { bounded: false, basis: 'unbounded', table: primary?.table ?? null };
349
- if (isUniqueBounded(tokens, level, from, primary, uniqueSetsOf(primary.table))) return { bounded: true, basis: 'unique' };
350
- return { bounded: false, basis: 'unbounded', table: primary.table };
327
+ const primary = from < 0 ? null : tableRef(tokens, from + 1);
328
+ const unbounded = { bounded: false, basis: 'unbounded', table: primary?.table ?? null };
329
+ const grouped = hasTopGroupBy(tokens, level);
330
+ if (tokens.some((token, index) => level[index] === 0 && (isWord(token, 'UNION') || isWord(token, 'INTERSECT') || isWord(token, 'EXCEPT')))) return unbounded;
331
+ if (unprovenProjectionCall(tokens, level, select, from)) return unbounded;
332
+ if (from < 0) return grouped ? unbounded : { bounded: true, basis: 'no from' };
333
+ if (!grouped && aggregateSelectItems(tokens, level, select, from).every(isAggregateItem)) return { bounded: true, basis: 'aggregate' };
334
+ if (grouped || !primary || primary.dynamic || cte.has(primary.table) || !singleFromRelation(tokens, level, from)) return unbounded;
335
+ if (isUniqueBounded(tokens, level, from, primary, uniqueSetsOf(primary.table, primary.schema))) return { bounded: true, basis: 'unique' };
336
+ return unbounded;
351
337
  }
352
338
 
353
339
  function recordSqlReference(list, ref, reads, cte, state) {
@@ -437,8 +423,8 @@ function statementAnalysis(tokens, uniqueSetsOf) {
437
423
  }
438
424
 
439
425
  /**
440
- * Read one SQL text. `uniqueSetsOf(table)` returns the unique-key column sets (each a lowercase column list) of the entity
441
- * of `table`, or null when no entity declares it.
426
+ * Read one SQL text. `uniqueSetsOf(table, schema)` returns the unique-key column sets (each a lowercase column list)
427
+ * proven for that relation by its declared schema authority, or null when no key is known.
442
428
  * Returns {writes: [{table, schema}], reads: [{table, schema}], selects: [{bounded, basis, table}], dynamic}.
443
429
  */
444
430
  export function analyzeSql(text, { uniqueSetsOf = () => null } = {}) {